From 88af94e990eda316097328ef586107664de1385f Mon Sep 17 00:00:00 2001 From: Ted McCormack Date: Sun, 21 May 2017 00:58:44 -0700 Subject: [PATCH 001/174] Added get_current_book, Fixed issue with segfault, Fixed issue with NoneType order --- GDAX/OrderBook.py | 368 +++++++++++++++++++++------------------- GDAX/WebsocketClient.py | 179 +++++++++---------- __init__.py | 0 3 files changed, 287 insertions(+), 260 deletions(-) create mode 100644 __init__.py diff --git a/GDAX/OrderBook.py b/GDAX/OrderBook.py index bc210e39..727829b8 100644 --- a/GDAX/OrderBook.py +++ b/GDAX/OrderBook.py @@ -13,177 +13,203 @@ class OrderBook(WebsocketClient): - def __init__(self, product_id='BTC-USD'): - WebsocketClient.__init__(self, products=product_id) - self._asks = RBTree() - self._bids = RBTree() - self._client = PublicClient(product_id=product_id) - self._sequence = -1 - - def onMessage(self, message): - sequence = message['sequence'] - if self._sequence == -1: - self._asks = RBTree() - self._bids = RBTree() - res = self._client.getProductOrderBook(level=3) - for bid in res['bids']: - self.add({ - 'id': bid[2], - 'side': 'buy', - 'price': float(bid[0]), - 'size': float(bid[1]) - }) - for ask in res['asks']: - self.add({ - 'id': ask[2], - 'side': 'sell', - 'price': float(ask[0]), - 'size': float(ask[1]) - }) - self._sequence = res['sequence'] - - if sequence <= self._sequence: - return - elif sequence > self._sequence + 1: - self.close() - self.start() - return - - # print(message) - msg_type = message['type'] - if msg_type == 'open': - self.add(message) - elif msg_type == 'done' and 'price' in message: - self.remove(message) - elif msg_type == 'match': - self.match(message) - elif msg_type == 'change': - self.change(message) - self._sequence = sequence - - # bid = self.get_bid() - # bids = self.get_bids(bid) - # bid_depth = sum([b['size'] for b in bids]) - # ask = self.get_ask() - # asks = self.get_asks(ask) - # ask_depth = sum([a['size'] for a in asks]) - # print('bid: %f @ %f - ask: %f @ %f' % (bid_depth, bid, ask_depth, ask)) - - def add(self, order): - order = { - 'id': order['order_id'] if 'order_id' in order else order['id'], - 'side': order['side'], - 'price': float(order['price']), - 'size': float(order['size']) if 'size' in order else float(order['remaining_size']) - } - if order['side'] == 'buy': - bids = self.get_bids(order['price']) - if bids is None: - bids = [order] - else: - bids.append(order) - self.set_bids(order['price'], bids) - else: - asks = self.get_asks(order['price']) - if asks is None: - asks = [order] - else: - asks.append(order) - self.set_asks(order['price'], asks) - - def remove(self, order): - price = float(order['price']) - if order['side'] == 'buy': - bids = self.get_bids(price) - if bids is not None: - bids = [o for o in bids if o['id'] != order['order_id']] - if len(bids) > 0: - self.set_bids(price, bids) - else: - self.remove_bids(price) - else: - asks = self.get_asks(price) - if asks is not None: - asks = [o for o in asks if o['id'] != order['order_id']] - if len(asks) > 0: - self.set_asks(price, asks) - else: - self.remove_asks(price) - - def match(self, order): - size = float(order['size']) - price = float(order['price']) - - if order['side'] == 'buy': - bids = self.get_bids(price) - assert bids[0]['id'] == order['maker_order_id'] - if bids[0]['size'] == size: - self.set_bids(price, bids[1:]) - else: - bids[0]['size'] -= size - self.set_bids(price, bids) - else: - asks = self.get_asks(price) - assert asks[0]['id'] == order['maker_order_id'] - if asks[0]['size'] == size: - self.set_asks(price, asks[1:]) - else: - asks[0]['size'] -= size - self.set_asks(price, asks) - - def change(self, order): - new_size = float(order['new_size']) - price = float(order['price']) - - if order['side'] == 'buy': - bids = self.get_bids(price) - if bids is None or not any(o['id'] == order['order_id'] for o in bids): - return - index = map(itemgetter('id'), bids).index(order['order_id']) - bids[index]['size'] = new_size - self.set_bids(price, bids) - else: - asks = self.get_asks(price) - if asks is None or not any(o['id'] == order['order_id'] for o in asks): - return - index = map(itemgetter('id'), asks).index(order['order_id']) - asks[index]['size'] = new_size - self.set_asks(price, asks) - - tree = self._asks if order['side'] == 'sell' else self._bids - node = tree.get(price) - - if node is None or not any(o['id'] == order['order_id'] for o in node): - return - - def get_ask(self): - return self._asks.min_key() - - def get_asks(self, price): - return self._asks.get(price) - - def remove_asks(self, price): - self._asks.remove(price) - - def set_asks(self, price, asks): - self._asks.insert(price, asks) - - def get_bid(self): - return self._bids.max_key() - - def get_bids(self, price): - return self._bids.get(price) - - def remove_bids(self, price): - self._bids.remove(price) - - def set_bids(self, price, bids): - self._bids.insert(price, bids) + def __init__(self, product_id='BTC-USD'): + WebsocketClient.__init__(self, products=product_id) + self._asks = RBTree() + self._bids = RBTree() + self._client = PublicClient(product_id=product_id) + self._sequence = -1 + + def onMessage(self, message): + sequence = message['sequence'] + if self._sequence == -1: + self._asks = RBTree() + self._bids = RBTree() + res = self._client.getProductOrderBook(level=3) + for bid in res['bids']: + self.add({ + 'id': bid[2], + 'side': 'buy', + 'price': float(bid[0]), + 'size': float(bid[1]) + }) + for ask in res['asks']: + self.add({ + 'id': ask[2], + 'side': 'sell', + 'price': float(ask[0]), + 'size': float(ask[1]) + }) + self._sequence = res['sequence'] + + if sequence <= self._sequence or sequence > self._sequence + 1: + print("Out of sequence!", sequence, self._sequence) + self._sequence = sequence + return + + # print(message) + msg_type = message['type'] + if msg_type == 'open': + self.add(message) + elif msg_type == 'done' and 'price' in message: + self.remove(message) + elif msg_type == 'match': + self.match(message) + elif msg_type == 'change': + self.change(message) + + self._sequence = sequence + + # bid = self.get_bid() + # bids = self.get_bids(bid) + # bid_depth = sum([b['size'] for b in bids]) + # ask = self.get_ask() + # asks = self.get_asks(ask) + # ask_depth = sum([a['size'] for a in asks]) + # print('bid: %f @ %f - ask: %f @ %f' % (bid_depth, bid, ask_depth, ask)) + + def add(self, order): + order = { + 'id': order['order_id'] if 'order_id' in order else order['id'], + 'side': order['side'], + 'price': float(order['price']), + 'size': float(order['size']) if 'size' in order else float(order['remaining_size']) + } + if order['side'] == 'buy': + bids = self.get_bids(order['price']) + if bids is None: + bids = [order] + else: + bids.append(order) + self.set_bids(order['price'], bids) + else: + asks = self.get_asks(order['price']) + if asks is None: + asks = [order] + else: + asks.append(order) + self.set_asks(order['price'], asks) + + def remove(self, order): + price = float(order['price']) + if order['side'] == 'buy': + bids = self.get_bids(price) + if bids is not None: + bids = [o for o in bids if o['id'] != order['order_id']] + if len(bids) > 0: + self.set_bids(price, bids) + else: + self.remove_bids(price) + else: + asks = self.get_asks(price) + if asks is not None: + asks = [o for o in asks if o['id'] != order['order_id']] + if len(asks) > 0: + self.set_asks(price, asks) + else: + self.remove_asks(price) + + def match(self, order): + size = float(order['size']) + price = float(order['price']) + + if order['side'] == 'buy': + bids = self.get_bids(price) + if not bids: + return + assert bids[0]['id'] == order['maker_order_id'] + if bids[0]['size'] == size: + self.set_bids(price, bids[1:]) + else: + bids[0]['size'] -= size + self.set_bids(price, bids) + else: + asks = self.get_asks(price) + if not asks: + return + assert asks[0]['id'] == order['maker_order_id'] + if asks[0]['size'] == size: + self.set_asks(price, asks[1:]) + else: + asks[0]['size'] -= size + self.set_asks(price, asks) + + def change(self, order): + new_size = float(order['new_size']) + price = float(order['price']) + + if order['side'] == 'buy': + bids = self.get_bids(price) + if bids is None or not any(o['id'] == order['order_id'] for o in bids): + return + index = map(itemgetter('id'), bids).index(order['order_id']) + bids[index]['size'] = new_size + self.set_bids(price, bids) + else: + asks = self.get_asks(price) + if asks is None or not any(o['id'] == order['order_id'] for o in asks): + return + index = map(itemgetter('id'), asks).index(order['order_id']) + asks[index]['size'] = new_size + self.set_asks(price, asks) + + tree = self._asks if order['side'] == 'sell' else self._bids + node = tree.get(price) + + if node is None or not any(o['id'] == order['order_id'] for o in node): + return + + def get_current_book(self): + result = dict() + result['sequence'] = self._sequence + result['asks'] = list() + result['bids'] = list() + for ask in self._asks: + thisAsk = self._asks[ask] + for order in thisAsk: + result['asks'].append([ + order['price'], + order['size'], + order['id'], + ]) + for bid in self._bids: + thisBid = self._bids[bid] + for order in thisBid: + result['bids'].append([ + order['price'], + order['size'], + order['id'], + ]) + return result + + def get_ask(self): + return self._asks.min_key() + + def get_asks(self, price): + return self._asks.get(price) + + def remove_asks(self, price): + self._asks.remove(price) + + def set_asks(self, price, asks): + self._asks.insert(price, asks) + + def get_bid(self): + return self._bids.max_key() + + def get_bids(self, price): + return self._bids.get(price) + + def remove_bids(self, price): + self._bids.remove(price) + + def set_bids(self, price, bids): + self._bids.insert(price, bids) if __name__ == '__main__': - import time - order_book = OrderBook() - order_book.start() - time.sleep(10) - order_book.close() + import time + order_book = OrderBook() + order_book.start() + time.sleep(10) + order_book.close() diff --git a/GDAX/WebsocketClient.py b/GDAX/WebsocketClient.py index bedad6a5..788e395c 100644 --- a/GDAX/WebsocketClient.py +++ b/GDAX/WebsocketClient.py @@ -11,95 +11,96 @@ from websocket import create_connection class WebsocketClient(object): - def __init__(self, url=None, products=None, type=None): - if url is None: - url = "wss://ws-feed.gdax.com" - - self.url = url - self.products = products - self.type = type or "subscribe" - self.stop = None - self.ws = None - self.thread = None - - def start(self): - def _go(): - self._connect() - self._listen() - - self.onOpen() - self.ws = create_connection(self.url) - self.thread = Thread(target=_go) - self.thread.start() - - def _connect(self): - if self.products is None: - self.products = ["BTC-USD"] - elif not isinstance(self.products, list): - self.products = [self.products] - - if self.url[-1] == "/": - self.url = self.url[:-1] - - self.stop = False - sub_params = {'type': 'subscribe', 'product_ids': self.products} - self.ws.send(json.dumps(sub_params)) - if self.type is "heartbeat": - sub_params = {"type": "heartbeat", "on": True} - self.ws.send(json.dumps(sub_params)) - - def _listen(self): - while not self.stop: - try: - msg = json.loads(self.ws.recv()) - except Exception as e: - self.onError(e) - self.close() - else: - self.onMessage(msg) - - def close(self): - if self.stop is False: - if self.type is "heartbeat": - self.ws.send(json.dumps({"type": "heartbeat", "on": False})) - self.stop = True - self.onClose() - self.ws.close() - - def onOpen(self): - print("-- Subscribed! --\n") - - def onClose(self): - print("\n-- Socket Closed --") - - def onMessage(self, msg): - print(msg) - - def onError(self, e): - SystemError(e) + def __init__(self, url=None, products=None, type=None): + if url is None: + url = "wss://ws-feed.gdax.com" + + self.url = url + self.products = products + self.type = "subscribe" #type or "subscribe" + self.stop = False + self.ws = None + self.thread = None + + def start(self): + def _go(): + self._connect() + self._listen() + + self.onOpen() + self.ws = create_connection(self.url) + self.thread = Thread(target=_go) + self.thread.start() + + def _connect(self): + if self.products is None: + self.products = ["BTC-USD"] + elif not isinstance(self.products, list): + self.products = [self.products] + + if self.url[-1] == "/": + self.url = self.url[:-1] + + self.stop = False + sub_params = {'type': 'subscribe', 'product_ids': self.products} + self.ws.send(json.dumps(sub_params)) + if self.type == "heartbeat": + sub_params = {"type": "heartbeat", "on": True} + self.ws.send(json.dumps(sub_params)) + + def _listen(self): + while not self.stop: + try: + msg = json.loads(self.ws.recv()) + except Exception as e: + self.onError(e) + self.close() + else: + self.onMessage(msg) + + def close(self): + if not self.stop: + if self.type == "heartbeat": + self.ws.send(json.dumps({"type": "heartbeat", "on": False})) + self.onClose() + + def onOpen(self): + print("-- Subscribed! --\n") + + def onClose(self): + self.stop = True + #self.thread = None + self.ws.close() + print("\n-- Socket Closed --") + + def onMessage(self, msg): + print(msg) + + def onError(self, e): + SystemError(e) if __name__ == "__main__": - import GDAX, time - class myWebsocketClient(GDAX.WebsocketClient): - def onOpen(self): - self.url = "wss://ws-feed.gdax.com/" - self.products = ["BTC-USD", "ETH-USD"] - self.MessageCount = 0 - print ("Lets count the messages!") - - def onMessage(self, msg): - print ("Message type:", msg["type"], "\t@ %.3f" % float(msg["price"])) - self.MessageCount += 1 - - def onClose(self): - print ("-- Goodbye! --") - - wsClient = myWebsocketClient() - wsClient.start() - print(wsClient.url, wsClient.products) - # Do some logic with the data - while (wsClient.MessageCount < 500): - print ("\nMessageCount =", "%i \n") % wsClient.MessageCount - time.sleep(1) - wsClient.close() + import GDAX, time + class myWebsocketClient(GDAX.WebsocketClient): + def onOpen(self): + self.url = "wss://ws-feed.gdax.com/" + self.products = ["BTC-USD", "ETH-USD"] + self.MessageCount = 0 + print ("Lets count the messages!") + + def onMessage(self, msg): + print ("Message type:", msg["type"], "\t@ %.3f" % float(msg["price"])) + self.MessageCount += 1 + + def onClose(self): + print ("-- Goodbye! --") + + wsClient = myWebsocketClient() + wsClient.start() + print(wsClient.url, wsClient.products) + # Do some logic with the data + while (wsClient.MessageCount < 500): + print ("\nMessageCount =", "%i \n") % wsClient.MessageCount + time.sleep(1) + wsClient.close() diff --git a/__init__.py b/__init__.py new file mode 100644 index 00000000..e69de29b From 21519190e2d6cc5c3f01b6b00c9ec1e0d819afec Mon Sep 17 00:00:00 2001 From: Ted McCormack Date: Sun, 21 May 2017 01:08:14 -0700 Subject: [PATCH 002/174] ALL YOUR SPACES BECOME THE TAB --- GDAX/AuthenticatedClient.py | 552 ++++++++++++++++++------------------ GDAX/PublicClient.py | 108 +++---- 2 files changed, 330 insertions(+), 330 deletions(-) diff --git a/GDAX/AuthenticatedClient.py b/GDAX/AuthenticatedClient.py index 4aa2a027..b9fa8d69 100644 --- a/GDAX/AuthenticatedClient.py +++ b/GDAX/AuthenticatedClient.py @@ -9,281 +9,281 @@ from GDAX.PublicClient import PublicClient class AuthenticatedClient(PublicClient): - def __init__(self, key, b64secret, passphrase, api_url="https://api.gdax.com", product_id="BTC-USD"): - self.url = api_url - if api_url[-1] == "/": - self.url = api_url[:-1] - self.productId = product_id - self.auth = GdaxAuth(key, b64secret, passphrase) - - def getAccount(self, accountId): - r = requests.get(self.url + '/accounts/' + accountId, auth=self.auth) - #r.raise_for_status() - return r.json() - - def getAccounts(self): - return self.getAccount('') - - def getAccountHistory(self, accountId): - list = [] - r = requests.get(self.url + '/accounts/%s/ledger' %accountId, auth=self.auth) - #r.raise_for_status() - list.append(r.json()) - if "cb-after" in r.headers: - self.historyPagination(accountId, list, r.headers["cb-after"]) - return list - - def historyPagination(self, accountId, list, after): - r = requests.get(self.url + '/accounts/%s/ledger?after=%s' %(accountId, str(after)), auth=self.auth) - #r.raise_for_status() - if r.json(): - list.append(r.json()) - if "cb-after" in r.headers: - self.historyPagination(accountId, list, r.headers["cb-after"]) - return list - - def getAccountHolds(self, accountId): - list = [] - r = requests.get(self.url + '/accounts/%s/holds' %accountId, auth=self.auth) - #r.raise_for_status() - list.append(r.json()) - if "cb-after" in r.headers: - self.holdsPagination(accountId, list, r.headers["cb-after"]) - return list - - def holdsPagination(self, accountId, list, after): - r = requests.get(self.url + '/accounts/%s/holds?after=%s' %(accountId, str(after)), auth=self.auth) - #r.raise_for_status() - if r.json(): - list.append(r.json()) - if "cb-after" in r.headers: - self.holdsPagination(accountId, list, r.headers["cb-after"]) - return list - - def buy(self, buyParams): - buyParams["side"] = "buy" - if not buyParams["product_id"]: - buyParams["product_id"] = self.productId - r = requests.post(self.url + '/orders', data=json.dumps(buyParams), auth=self.auth) - #r.raise_for_status() - return r.json() - - def sell(self, sellParams): - sellParams["side"] = "sell" - r = requests.post(self.url + '/orders', data=json.dumps(sellParams), auth=self.auth) - #r.raise_for_status() - return r.json() - - def cancelOrder(self, orderId): - r = requests.delete(self.url + '/orders/' + orderId, auth=self.auth) - #r.raise_for_status() - return r.json() - - def cancelAll(self, data=None, product=''): - if type(data) is dict: - if "product" in data: product = data["product"] - r = requests.delete(self.url + '/orders/', data=json.dumps({'product_id':product or self.productId}), auth=self.auth) - #r.raise_for_status() - return r.json() - - def getOrder(self, orderId): - r = requests.get(self.url + '/orders/' + orderId, auth=self.auth) - #r.raise_for_status() - return r.json() - - def getOrders(self): - list = [] - r = requests.get(self.url + '/orders/', auth=self.auth) - #r.raise_for_status() - list.append(r.json()) - if 'cb-after' in r.headers: - self.paginateOrders(list, r.headers['cb-after']) - return list - - def paginateOrders(self, list, after): - r = requests.get(self.url + '/orders?after=%s' %str(after)) - #r.raise_for_status() - if r.json(): - list.append(r.json()) - if 'cb-after' in r.headers: - self.paginateOrders(list, r.headers['cb-after']) - return list - - def getFills(self, orderId='', productId='', before='', after='', limit=''): - list = [] - url = self.url + '/fills?' - if orderId: url += "order_id=%s&" %str(orderId) - if productId: url += "product_id=%s&" %(productId or self.productId) - if before: url += "before=%s&" %str(before) - if after: url += "after=%s&" %str(after) - if limit: url += "limit=%s&" %str(limit) - r = requests.get(url, auth=self.auth) - #r.raise_for_status() - list.append(r.json()) - if 'cb-after' in r.headers and limit is not len(r.json()): - return self.paginateFills(list, r.headers['cb-after'], orderId=orderId, productId=productId) - return list - - def paginateFills(self, list, after, orderId='', productId=''): - url = self.url + '/fills?after=%s&' % str(after) - if orderId: url += "order_id=%s&" % str(orderId) - if productId: url += "product_id=%s&" % (productId or self.productId) - r = requests.get(url, auth=self.auth) - #r.raise_for_status() - if r.json(): - list.append(r.json()) - if 'cb-after' in r.headers: - return self.paginateFills(list, r.headers['cb-after'], orderId=orderId, productId=productId) - return list - - def getFundings(self, list='', status='', after=''): - if not list: list = [] - url = self.url + '/funding?' - if status: url += "status=%s&" % str(status) - if after: url += 'after=%s&' % str(after) - r = requests.get(url, auth=self.auth) - #r.raise_for_status() - list.append(r.json()) - if 'cb-after' in r.headers: - return self.getFundings(list, status=status, after=r.headers['cb-after']) - return list - - def repayFunding(self, amount='', currency=''): - payload = { - "amount": amount, - "currency": currency #example: USD - } - r = requests.post(self.url + "/funding/repay", data=json.dumps(payload), auth=self.auth) - #r.raise_for_status() - return r.json() - - def marginTransfer(self, margin_profile_id="", type="",currency="",amount=""): - payload = { - "margin_profile_id": margin_profile_id, - "type": type, - "currency": currency, # example: USD - "amount": amount - } - r = requests.post(self.url + "/profiles/margin-transfer", data=json.dumps(payload), auth=self.auth) - # r.raise_for_status() - return r.json() - - def getPosition(self): - r = requests.get(self.url + "/position", auth=self.auth) - # r.raise_for_status() - return r.json() - - def closePosition(self, repay_only=""): - payload = { - "repay_only": repay_only or False - } - r = requests.post(self.url + "/position/close", data=json.dumps(payload), auth=self.auth) - # r.raise_for_status() - return r.json() - - def deposit(self, amount="", currency="", payment_method_id=""): - payload = { - "amount": amount, - "currency": currency, - "payment_method_id": payment_method_id - } - r = requests.post(self.url + "/deposits/payment-method", data=json.dumps(payload), auth=self.auth) - #r.raise_for_status() - return r.json() - - def coinbaseDeposit(self, amount="", currency="", coinbase_account_id=""): - payload = { - "amount": amount, - "currency": currency, - "coinbase_account_id": coinbase_account_id - } - r = requests.post(self.url + "/deposits/coinbase-account", data=json.dumps(payload), auth=self.auth) - # r.raise_for_status() - return r.json() - - def withdraw(self, amount="", currency="", payment_method_id=""): - payload = { - "amount": amount, - "currency": currency, - "payment_method_id": payment_method_id - } - r = requests.post(self.url + "/withdrawals/payment-method", data=json.dumps(payload), auth=self.auth) - #r.raise_for_status() - return r.json() - - def coinbaseWithdraw(self, amount="", currency="", coinbase_account_id=""): - payload = { - "amount": amount, - "currency": currency, - "coinbase_account_id": coinbase_account_id - } - r = requests.post(self.url + "/withdrawals/coinbase", data=json.dumps(payload), auth=self.auth) - # r.raise_for_status() - return r.json() - - def cryptoWithdraw(self, amount="", currency="", crypto_address=""): - payload = { - "amount": amount, - "currency": currency, - "crypto_address": crypto_address - } - r = requests.post(self.url + "/withdrawals/crypto", data=json.dumps(payload), auth=self.auth) - # r.raise_for_status() - return r.json() - - def getPaymentMethods(self): - r = requests.get(self.url + "/payment-methods", auth=self.auth) - #r.raise_for_status() - return r.json() - - def getCoinbaseAccounts(self): - r = requests.get(self.url + "/coinbase-accounts", auth=self.auth) - #r.raise_for_status() - return r.json() - - def createReport(self, type="", start_date="", end_date="", product_id="", account_id="", format="", email=""): - payload = { - "type": type, - "start_date": start_date, - "end_date": end_date, - "product_id": product_id, - "account_id": account_id, - "format": format, - "email": email - } - r = requests.post(self.url + "/reports", data=json.dumps(payload), auth=self.auth) - #r.raise_for_status() - return r.json() - - def getReport(self, reportId=""): - r = requests.get(self.url + "/reports/" + reportId, auth=self.auth) - #r.raise_for_status() - return r.json() - - def getTrailingVolume(self): - r = requests.get(self.url + "/users/self/trailing-volume", auth=self.auth) - #r.raise_for_status() - return r.json() + def __init__(self, key, b64secret, passphrase, api_url="https://api.gdax.com", product_id="BTC-USD"): + self.url = api_url + if api_url[-1] == "/": + self.url = api_url[:-1] + self.productId = product_id + self.auth = GdaxAuth(key, b64secret, passphrase) + + def getAccount(self, accountId): + r = requests.get(self.url + '/accounts/' + accountId, auth=self.auth) + #r.raise_for_status() + return r.json() + + def getAccounts(self): + return self.getAccount('') + + def getAccountHistory(self, accountId): + list = [] + r = requests.get(self.url + '/accounts/%s/ledger' %accountId, auth=self.auth) + #r.raise_for_status() + list.append(r.json()) + if "cb-after" in r.headers: + self.historyPagination(accountId, list, r.headers["cb-after"]) + return list + + def historyPagination(self, accountId, list, after): + r = requests.get(self.url + '/accounts/%s/ledger?after=%s' %(accountId, str(after)), auth=self.auth) + #r.raise_for_status() + if r.json(): + list.append(r.json()) + if "cb-after" in r.headers: + self.historyPagination(accountId, list, r.headers["cb-after"]) + return list + + def getAccountHolds(self, accountId): + list = [] + r = requests.get(self.url + '/accounts/%s/holds' %accountId, auth=self.auth) + #r.raise_for_status() + list.append(r.json()) + if "cb-after" in r.headers: + self.holdsPagination(accountId, list, r.headers["cb-after"]) + return list + + def holdsPagination(self, accountId, list, after): + r = requests.get(self.url + '/accounts/%s/holds?after=%s' %(accountId, str(after)), auth=self.auth) + #r.raise_for_status() + if r.json(): + list.append(r.json()) + if "cb-after" in r.headers: + self.holdsPagination(accountId, list, r.headers["cb-after"]) + return list + + def buy(self, buyParams): + buyParams["side"] = "buy" + if not buyParams["product_id"]: + buyParams["product_id"] = self.productId + r = requests.post(self.url + '/orders', data=json.dumps(buyParams), auth=self.auth) + #r.raise_for_status() + return r.json() + + def sell(self, sellParams): + sellParams["side"] = "sell" + r = requests.post(self.url + '/orders', data=json.dumps(sellParams), auth=self.auth) + #r.raise_for_status() + return r.json() + + def cancelOrder(self, orderId): + r = requests.delete(self.url + '/orders/' + orderId, auth=self.auth) + #r.raise_for_status() + return r.json() + + def cancelAll(self, data=None, product=''): + if type(data) is dict: + if "product" in data: product = data["product"] + r = requests.delete(self.url + '/orders/', data=json.dumps({'product_id':product or self.productId}), auth=self.auth) + #r.raise_for_status() + return r.json() + + def getOrder(self, orderId): + r = requests.get(self.url + '/orders/' + orderId, auth=self.auth) + #r.raise_for_status() + return r.json() + + def getOrders(self): + list = [] + r = requests.get(self.url + '/orders/', auth=self.auth) + #r.raise_for_status() + list.append(r.json()) + if 'cb-after' in r.headers: + self.paginateOrders(list, r.headers['cb-after']) + return list + + def paginateOrders(self, list, after): + r = requests.get(self.url + '/orders?after=%s' %str(after)) + #r.raise_for_status() + if r.json(): + list.append(r.json()) + if 'cb-after' in r.headers: + self.paginateOrders(list, r.headers['cb-after']) + return list + + def getFills(self, orderId='', productId='', before='', after='', limit=''): + list = [] + url = self.url + '/fills?' + if orderId: url += "order_id=%s&" %str(orderId) + if productId: url += "product_id=%s&" %(productId or self.productId) + if before: url += "before=%s&" %str(before) + if after: url += "after=%s&" %str(after) + if limit: url += "limit=%s&" %str(limit) + r = requests.get(url, auth=self.auth) + #r.raise_for_status() + list.append(r.json()) + if 'cb-after' in r.headers and limit is not len(r.json()): + return self.paginateFills(list, r.headers['cb-after'], orderId=orderId, productId=productId) + return list + + def paginateFills(self, list, after, orderId='', productId=''): + url = self.url + '/fills?after=%s&' % str(after) + if orderId: url += "order_id=%s&" % str(orderId) + if productId: url += "product_id=%s&" % (productId or self.productId) + r = requests.get(url, auth=self.auth) + #r.raise_for_status() + if r.json(): + list.append(r.json()) + if 'cb-after' in r.headers: + return self.paginateFills(list, r.headers['cb-after'], orderId=orderId, productId=productId) + return list + + def getFundings(self, list='', status='', after=''): + if not list: list = [] + url = self.url + '/funding?' + if status: url += "status=%s&" % str(status) + if after: url += 'after=%s&' % str(after) + r = requests.get(url, auth=self.auth) + #r.raise_for_status() + list.append(r.json()) + if 'cb-after' in r.headers: + return self.getFundings(list, status=status, after=r.headers['cb-after']) + return list + + def repayFunding(self, amount='', currency=''): + payload = { + "amount": amount, + "currency": currency #example: USD + } + r = requests.post(self.url + "/funding/repay", data=json.dumps(payload), auth=self.auth) + #r.raise_for_status() + return r.json() + + def marginTransfer(self, margin_profile_id="", type="",currency="",amount=""): + payload = { + "margin_profile_id": margin_profile_id, + "type": type, + "currency": currency, # example: USD + "amount": amount + } + r = requests.post(self.url + "/profiles/margin-transfer", data=json.dumps(payload), auth=self.auth) + # r.raise_for_status() + return r.json() + + def getPosition(self): + r = requests.get(self.url + "/position", auth=self.auth) + # r.raise_for_status() + return r.json() + + def closePosition(self, repay_only=""): + payload = { + "repay_only": repay_only or False + } + r = requests.post(self.url + "/position/close", data=json.dumps(payload), auth=self.auth) + # r.raise_for_status() + return r.json() + + def deposit(self, amount="", currency="", payment_method_id=""): + payload = { + "amount": amount, + "currency": currency, + "payment_method_id": payment_method_id + } + r = requests.post(self.url + "/deposits/payment-method", data=json.dumps(payload), auth=self.auth) + #r.raise_for_status() + return r.json() + + def coinbaseDeposit(self, amount="", currency="", coinbase_account_id=""): + payload = { + "amount": amount, + "currency": currency, + "coinbase_account_id": coinbase_account_id + } + r = requests.post(self.url + "/deposits/coinbase-account", data=json.dumps(payload), auth=self.auth) + # r.raise_for_status() + return r.json() + + def withdraw(self, amount="", currency="", payment_method_id=""): + payload = { + "amount": amount, + "currency": currency, + "payment_method_id": payment_method_id + } + r = requests.post(self.url + "/withdrawals/payment-method", data=json.dumps(payload), auth=self.auth) + #r.raise_for_status() + return r.json() + + def coinbaseWithdraw(self, amount="", currency="", coinbase_account_id=""): + payload = { + "amount": amount, + "currency": currency, + "coinbase_account_id": coinbase_account_id + } + r = requests.post(self.url + "/withdrawals/coinbase", data=json.dumps(payload), auth=self.auth) + # r.raise_for_status() + return r.json() + + def cryptoWithdraw(self, amount="", currency="", crypto_address=""): + payload = { + "amount": amount, + "currency": currency, + "crypto_address": crypto_address + } + r = requests.post(self.url + "/withdrawals/crypto", data=json.dumps(payload), auth=self.auth) + # r.raise_for_status() + return r.json() + + def getPaymentMethods(self): + r = requests.get(self.url + "/payment-methods", auth=self.auth) + #r.raise_for_status() + return r.json() + + def getCoinbaseAccounts(self): + r = requests.get(self.url + "/coinbase-accounts", auth=self.auth) + #r.raise_for_status() + return r.json() + + def createReport(self, type="", start_date="", end_date="", product_id="", account_id="", format="", email=""): + payload = { + "type": type, + "start_date": start_date, + "end_date": end_date, + "product_id": product_id, + "account_id": account_id, + "format": format, + "email": email + } + r = requests.post(self.url + "/reports", data=json.dumps(payload), auth=self.auth) + #r.raise_for_status() + return r.json() + + def getReport(self, reportId=""): + r = requests.get(self.url + "/reports/" + reportId, auth=self.auth) + #r.raise_for_status() + return r.json() + + def getTrailingVolume(self): + r = requests.get(self.url + "/users/self/trailing-volume", auth=self.auth) + #r.raise_for_status() + return r.json() class GdaxAuth(AuthBase): - # Provided by GDAX: https://docs.gdax.com/#signing-a-message - def __init__(self, api_key, secret_key, passphrase): - self.api_key = api_key - self.secret_key = secret_key - self.passphrase = passphrase - - def __call__(self, request): - timestamp = str(time.time()) - message = timestamp + request.method + request.path_url + (request.body or '') - message = message.encode('ascii') - hmac_key = base64.b64decode(self.secret_key) - signature = hmac.new(hmac_key, message, hashlib.sha256) - signature_b64 = base64.b64encode(signature.digest()) - request.headers.update({ - 'Content-Type': 'Application/JSON', - 'CB-ACCESS-SIGN': signature_b64, - 'CB-ACCESS-TIMESTAMP': timestamp, - 'CB-ACCESS-KEY': self.api_key, - 'CB-ACCESS-PASSPHRASE': self.passphrase - }) - return request + # Provided by GDAX: https://docs.gdax.com/#signing-a-message + def __init__(self, api_key, secret_key, passphrase): + self.api_key = api_key + self.secret_key = secret_key + self.passphrase = passphrase + + def __call__(self, request): + timestamp = str(time.time()) + message = timestamp + request.method + request.path_url + (request.body or '') + message = message.encode('ascii') + hmac_key = base64.b64decode(self.secret_key) + signature = hmac.new(hmac_key, message, hashlib.sha256) + signature_b64 = base64.b64encode(signature.digest()) + request.headers.update({ + 'Content-Type': 'Application/JSON', + 'CB-ACCESS-SIGN': signature_b64, + 'CB-ACCESS-TIMESTAMP': timestamp, + 'CB-ACCESS-KEY': self.api_key, + 'CB-ACCESS-PASSPHRASE': self.passphrase + }) + return request diff --git a/GDAX/PublicClient.py b/GDAX/PublicClient.py index fda7f8a9..d98dc72a 100644 --- a/GDAX/PublicClient.py +++ b/GDAX/PublicClient.py @@ -7,65 +7,65 @@ import requests class PublicClient(): - def __init__(self, api_url="https://api.gdax.com", product_id="BTC-USD"): - self.url = api_url - if api_url[-1] == "/": - self.url = api_url[:-1] - self.productId = product_id + def __init__(self, api_url="https://api.gdax.com", product_id="BTC-USD"): + self.url = api_url + if api_url[-1] == "/": + self.url = api_url[:-1] + self.productId = product_id - def getProducts(self): - r = requests.get(self.url + '/products') - #r.raise_for_status() - return r.json() + def getProducts(self): + r = requests.get(self.url + '/products') + #r.raise_for_status() + return r.json() - def getProductOrderBook(self, json=None, level=2, product=''): - if type(json) is dict: - if "product" in json: product = json["product"] - if "level" in json: level = json['level'] - r = requests.get(self.url + '/products/%s/book?level=%s' % (product or self.productId, str(level))) - #r.raise_for_status() - return r.json() + def getProductOrderBook(self, json=None, level=2, product=''): + if type(json) is dict: + if "product" in json: product = json["product"] + if "level" in json: level = json['level'] + r = requests.get(self.url + '/products/%s/book?level=%s' % (product or self.productId, str(level))) + #r.raise_for_status() + return r.json() - def getProductTicker(self, json=None, product=''): - if type(json) is dict: - if "product" in json: product = json["product"] - r = requests.get(self.url + '/products/%s/ticker' % (product or self.productId)) - #r.raise_for_status() - return r.json() + def getProductTicker(self, json=None, product=''): + if type(json) is dict: + if "product" in json: product = json["product"] + r = requests.get(self.url + '/products/%s/ticker' % (product or self.productId)) + #r.raise_for_status() + return r.json() - def getProductTrades(self, json=None, product=''): - if type(json) is dict: - if "product" in json: product = json["product"] - r = requests.get(self.url + '/products/%s/trades' % (product or self.productId)) - #r.raise_for_status() - return r.json() + def getProductTrades(self, json=None, product=''): + if type(json) is dict: + if "product" in json: product = json["product"] + r = requests.get(self.url + '/products/%s/trades' % (product or self.productId)) + #r.raise_for_status() + return r.json() - def getProductHistoricRates(self, json=None, product='', start='', end='', granularity=''): - payload = {} - if type(json) is dict: - if "product" in json: product = json["product"] - payload = json - else: - payload["start"] = start - payload["end"] = end - payload["granularity"] = granularity - r = requests.get(self.url + '/products/%s/candles' % (product or self.productId), params=payload) - #r.raise_for_status() - return r.json() + def getProductHistoricRates(self, json=None, product='', start='', end='', granularity=''): + payload = {} + if type(json) is dict: + if "product" in json: product = json["product"] + payload = json + else: + payload["start"] = start + payload["end"] = end + payload["granularity"] = granularity + r = requests.get(self.url + '/products/%s/candles' % (product or self.productId), params=payload) + #r.raise_for_status() + return r.json() - def getProduct24HrStats(self, json=None, product=''): - if type(json) is dict: - if "product" in json: product = json["product"] - r = requests.get(self.url + '/products/%s/stats' % (product or self.productId)) - #r.raise_for_status() - return r.json() + def getProduct24HrStats(self, json=None, product=''): + if type(json) is dict: + if "product" in json: product = json["product"] + r = requests.get(self.url + '/products/%s/stats' % (product or self.productId)) + #r.raise_for_status() + return r.json() - def getCurrencies(self): - r = requests.get(self.url + '/currencies') - #r.raise_for_status() - return r.json() + def getCurrencies(self): + r = requests.get(self.url + '/currencies') + #r.raise_for_status() + return r.json() - def getTime(self): - r = requests.get(self.url + '/time') - #r.raise_for_status() - return r.json() + def getTime(self): + r = requests.get(self.url + '/time') + #r.raise_for_status() + return r.json() From 150e87da3807b8a86f7b34c8096e07385e64c441 Mon Sep 17 00:00:00 2001 From: Ted McCormack Date: Sun, 21 May 2017 01:18:35 -0700 Subject: [PATCH 003/174] because all your space are belong to me --- GDAX/AuthenticatedClient.py | 552 ++++++++++++++++++------------------ GDAX/OrderBook.py | 394 ++++++++++++------------- GDAX/PublicClient.py | 108 +++---- GDAX/WebsocketClient.py | 180 ++++++------ 4 files changed, 617 insertions(+), 617 deletions(-) diff --git a/GDAX/AuthenticatedClient.py b/GDAX/AuthenticatedClient.py index b9fa8d69..4aa2a027 100644 --- a/GDAX/AuthenticatedClient.py +++ b/GDAX/AuthenticatedClient.py @@ -9,281 +9,281 @@ from GDAX.PublicClient import PublicClient class AuthenticatedClient(PublicClient): - def __init__(self, key, b64secret, passphrase, api_url="https://api.gdax.com", product_id="BTC-USD"): - self.url = api_url - if api_url[-1] == "/": - self.url = api_url[:-1] - self.productId = product_id - self.auth = GdaxAuth(key, b64secret, passphrase) - - def getAccount(self, accountId): - r = requests.get(self.url + '/accounts/' + accountId, auth=self.auth) - #r.raise_for_status() - return r.json() - - def getAccounts(self): - return self.getAccount('') - - def getAccountHistory(self, accountId): - list = [] - r = requests.get(self.url + '/accounts/%s/ledger' %accountId, auth=self.auth) - #r.raise_for_status() - list.append(r.json()) - if "cb-after" in r.headers: - self.historyPagination(accountId, list, r.headers["cb-after"]) - return list - - def historyPagination(self, accountId, list, after): - r = requests.get(self.url + '/accounts/%s/ledger?after=%s' %(accountId, str(after)), auth=self.auth) - #r.raise_for_status() - if r.json(): - list.append(r.json()) - if "cb-after" in r.headers: - self.historyPagination(accountId, list, r.headers["cb-after"]) - return list - - def getAccountHolds(self, accountId): - list = [] - r = requests.get(self.url + '/accounts/%s/holds' %accountId, auth=self.auth) - #r.raise_for_status() - list.append(r.json()) - if "cb-after" in r.headers: - self.holdsPagination(accountId, list, r.headers["cb-after"]) - return list - - def holdsPagination(self, accountId, list, after): - r = requests.get(self.url + '/accounts/%s/holds?after=%s' %(accountId, str(after)), auth=self.auth) - #r.raise_for_status() - if r.json(): - list.append(r.json()) - if "cb-after" in r.headers: - self.holdsPagination(accountId, list, r.headers["cb-after"]) - return list - - def buy(self, buyParams): - buyParams["side"] = "buy" - if not buyParams["product_id"]: - buyParams["product_id"] = self.productId - r = requests.post(self.url + '/orders', data=json.dumps(buyParams), auth=self.auth) - #r.raise_for_status() - return r.json() - - def sell(self, sellParams): - sellParams["side"] = "sell" - r = requests.post(self.url + '/orders', data=json.dumps(sellParams), auth=self.auth) - #r.raise_for_status() - return r.json() - - def cancelOrder(self, orderId): - r = requests.delete(self.url + '/orders/' + orderId, auth=self.auth) - #r.raise_for_status() - return r.json() - - def cancelAll(self, data=None, product=''): - if type(data) is dict: - if "product" in data: product = data["product"] - r = requests.delete(self.url + '/orders/', data=json.dumps({'product_id':product or self.productId}), auth=self.auth) - #r.raise_for_status() - return r.json() - - def getOrder(self, orderId): - r = requests.get(self.url + '/orders/' + orderId, auth=self.auth) - #r.raise_for_status() - return r.json() - - def getOrders(self): - list = [] - r = requests.get(self.url + '/orders/', auth=self.auth) - #r.raise_for_status() - list.append(r.json()) - if 'cb-after' in r.headers: - self.paginateOrders(list, r.headers['cb-after']) - return list - - def paginateOrders(self, list, after): - r = requests.get(self.url + '/orders?after=%s' %str(after)) - #r.raise_for_status() - if r.json(): - list.append(r.json()) - if 'cb-after' in r.headers: - self.paginateOrders(list, r.headers['cb-after']) - return list - - def getFills(self, orderId='', productId='', before='', after='', limit=''): - list = [] - url = self.url + '/fills?' - if orderId: url += "order_id=%s&" %str(orderId) - if productId: url += "product_id=%s&" %(productId or self.productId) - if before: url += "before=%s&" %str(before) - if after: url += "after=%s&" %str(after) - if limit: url += "limit=%s&" %str(limit) - r = requests.get(url, auth=self.auth) - #r.raise_for_status() - list.append(r.json()) - if 'cb-after' in r.headers and limit is not len(r.json()): - return self.paginateFills(list, r.headers['cb-after'], orderId=orderId, productId=productId) - return list - - def paginateFills(self, list, after, orderId='', productId=''): - url = self.url + '/fills?after=%s&' % str(after) - if orderId: url += "order_id=%s&" % str(orderId) - if productId: url += "product_id=%s&" % (productId or self.productId) - r = requests.get(url, auth=self.auth) - #r.raise_for_status() - if r.json(): - list.append(r.json()) - if 'cb-after' in r.headers: - return self.paginateFills(list, r.headers['cb-after'], orderId=orderId, productId=productId) - return list - - def getFundings(self, list='', status='', after=''): - if not list: list = [] - url = self.url + '/funding?' - if status: url += "status=%s&" % str(status) - if after: url += 'after=%s&' % str(after) - r = requests.get(url, auth=self.auth) - #r.raise_for_status() - list.append(r.json()) - if 'cb-after' in r.headers: - return self.getFundings(list, status=status, after=r.headers['cb-after']) - return list - - def repayFunding(self, amount='', currency=''): - payload = { - "amount": amount, - "currency": currency #example: USD - } - r = requests.post(self.url + "/funding/repay", data=json.dumps(payload), auth=self.auth) - #r.raise_for_status() - return r.json() - - def marginTransfer(self, margin_profile_id="", type="",currency="",amount=""): - payload = { - "margin_profile_id": margin_profile_id, - "type": type, - "currency": currency, # example: USD - "amount": amount - } - r = requests.post(self.url + "/profiles/margin-transfer", data=json.dumps(payload), auth=self.auth) - # r.raise_for_status() - return r.json() - - def getPosition(self): - r = requests.get(self.url + "/position", auth=self.auth) - # r.raise_for_status() - return r.json() - - def closePosition(self, repay_only=""): - payload = { - "repay_only": repay_only or False - } - r = requests.post(self.url + "/position/close", data=json.dumps(payload), auth=self.auth) - # r.raise_for_status() - return r.json() - - def deposit(self, amount="", currency="", payment_method_id=""): - payload = { - "amount": amount, - "currency": currency, - "payment_method_id": payment_method_id - } - r = requests.post(self.url + "/deposits/payment-method", data=json.dumps(payload), auth=self.auth) - #r.raise_for_status() - return r.json() - - def coinbaseDeposit(self, amount="", currency="", coinbase_account_id=""): - payload = { - "amount": amount, - "currency": currency, - "coinbase_account_id": coinbase_account_id - } - r = requests.post(self.url + "/deposits/coinbase-account", data=json.dumps(payload), auth=self.auth) - # r.raise_for_status() - return r.json() - - def withdraw(self, amount="", currency="", payment_method_id=""): - payload = { - "amount": amount, - "currency": currency, - "payment_method_id": payment_method_id - } - r = requests.post(self.url + "/withdrawals/payment-method", data=json.dumps(payload), auth=self.auth) - #r.raise_for_status() - return r.json() - - def coinbaseWithdraw(self, amount="", currency="", coinbase_account_id=""): - payload = { - "amount": amount, - "currency": currency, - "coinbase_account_id": coinbase_account_id - } - r = requests.post(self.url + "/withdrawals/coinbase", data=json.dumps(payload), auth=self.auth) - # r.raise_for_status() - return r.json() - - def cryptoWithdraw(self, amount="", currency="", crypto_address=""): - payload = { - "amount": amount, - "currency": currency, - "crypto_address": crypto_address - } - r = requests.post(self.url + "/withdrawals/crypto", data=json.dumps(payload), auth=self.auth) - # r.raise_for_status() - return r.json() - - def getPaymentMethods(self): - r = requests.get(self.url + "/payment-methods", auth=self.auth) - #r.raise_for_status() - return r.json() - - def getCoinbaseAccounts(self): - r = requests.get(self.url + "/coinbase-accounts", auth=self.auth) - #r.raise_for_status() - return r.json() - - def createReport(self, type="", start_date="", end_date="", product_id="", account_id="", format="", email=""): - payload = { - "type": type, - "start_date": start_date, - "end_date": end_date, - "product_id": product_id, - "account_id": account_id, - "format": format, - "email": email - } - r = requests.post(self.url + "/reports", data=json.dumps(payload), auth=self.auth) - #r.raise_for_status() - return r.json() - - def getReport(self, reportId=""): - r = requests.get(self.url + "/reports/" + reportId, auth=self.auth) - #r.raise_for_status() - return r.json() - - def getTrailingVolume(self): - r = requests.get(self.url + "/users/self/trailing-volume", auth=self.auth) - #r.raise_for_status() - return r.json() + def __init__(self, key, b64secret, passphrase, api_url="https://api.gdax.com", product_id="BTC-USD"): + self.url = api_url + if api_url[-1] == "/": + self.url = api_url[:-1] + self.productId = product_id + self.auth = GdaxAuth(key, b64secret, passphrase) + + def getAccount(self, accountId): + r = requests.get(self.url + '/accounts/' + accountId, auth=self.auth) + #r.raise_for_status() + return r.json() + + def getAccounts(self): + return self.getAccount('') + + def getAccountHistory(self, accountId): + list = [] + r = requests.get(self.url + '/accounts/%s/ledger' %accountId, auth=self.auth) + #r.raise_for_status() + list.append(r.json()) + if "cb-after" in r.headers: + self.historyPagination(accountId, list, r.headers["cb-after"]) + return list + + def historyPagination(self, accountId, list, after): + r = requests.get(self.url + '/accounts/%s/ledger?after=%s' %(accountId, str(after)), auth=self.auth) + #r.raise_for_status() + if r.json(): + list.append(r.json()) + if "cb-after" in r.headers: + self.historyPagination(accountId, list, r.headers["cb-after"]) + return list + + def getAccountHolds(self, accountId): + list = [] + r = requests.get(self.url + '/accounts/%s/holds' %accountId, auth=self.auth) + #r.raise_for_status() + list.append(r.json()) + if "cb-after" in r.headers: + self.holdsPagination(accountId, list, r.headers["cb-after"]) + return list + + def holdsPagination(self, accountId, list, after): + r = requests.get(self.url + '/accounts/%s/holds?after=%s' %(accountId, str(after)), auth=self.auth) + #r.raise_for_status() + if r.json(): + list.append(r.json()) + if "cb-after" in r.headers: + self.holdsPagination(accountId, list, r.headers["cb-after"]) + return list + + def buy(self, buyParams): + buyParams["side"] = "buy" + if not buyParams["product_id"]: + buyParams["product_id"] = self.productId + r = requests.post(self.url + '/orders', data=json.dumps(buyParams), auth=self.auth) + #r.raise_for_status() + return r.json() + + def sell(self, sellParams): + sellParams["side"] = "sell" + r = requests.post(self.url + '/orders', data=json.dumps(sellParams), auth=self.auth) + #r.raise_for_status() + return r.json() + + def cancelOrder(self, orderId): + r = requests.delete(self.url + '/orders/' + orderId, auth=self.auth) + #r.raise_for_status() + return r.json() + + def cancelAll(self, data=None, product=''): + if type(data) is dict: + if "product" in data: product = data["product"] + r = requests.delete(self.url + '/orders/', data=json.dumps({'product_id':product or self.productId}), auth=self.auth) + #r.raise_for_status() + return r.json() + + def getOrder(self, orderId): + r = requests.get(self.url + '/orders/' + orderId, auth=self.auth) + #r.raise_for_status() + return r.json() + + def getOrders(self): + list = [] + r = requests.get(self.url + '/orders/', auth=self.auth) + #r.raise_for_status() + list.append(r.json()) + if 'cb-after' in r.headers: + self.paginateOrders(list, r.headers['cb-after']) + return list + + def paginateOrders(self, list, after): + r = requests.get(self.url + '/orders?after=%s' %str(after)) + #r.raise_for_status() + if r.json(): + list.append(r.json()) + if 'cb-after' in r.headers: + self.paginateOrders(list, r.headers['cb-after']) + return list + + def getFills(self, orderId='', productId='', before='', after='', limit=''): + list = [] + url = self.url + '/fills?' + if orderId: url += "order_id=%s&" %str(orderId) + if productId: url += "product_id=%s&" %(productId or self.productId) + if before: url += "before=%s&" %str(before) + if after: url += "after=%s&" %str(after) + if limit: url += "limit=%s&" %str(limit) + r = requests.get(url, auth=self.auth) + #r.raise_for_status() + list.append(r.json()) + if 'cb-after' in r.headers and limit is not len(r.json()): + return self.paginateFills(list, r.headers['cb-after'], orderId=orderId, productId=productId) + return list + + def paginateFills(self, list, after, orderId='', productId=''): + url = self.url + '/fills?after=%s&' % str(after) + if orderId: url += "order_id=%s&" % str(orderId) + if productId: url += "product_id=%s&" % (productId or self.productId) + r = requests.get(url, auth=self.auth) + #r.raise_for_status() + if r.json(): + list.append(r.json()) + if 'cb-after' in r.headers: + return self.paginateFills(list, r.headers['cb-after'], orderId=orderId, productId=productId) + return list + + def getFundings(self, list='', status='', after=''): + if not list: list = [] + url = self.url + '/funding?' + if status: url += "status=%s&" % str(status) + if after: url += 'after=%s&' % str(after) + r = requests.get(url, auth=self.auth) + #r.raise_for_status() + list.append(r.json()) + if 'cb-after' in r.headers: + return self.getFundings(list, status=status, after=r.headers['cb-after']) + return list + + def repayFunding(self, amount='', currency=''): + payload = { + "amount": amount, + "currency": currency #example: USD + } + r = requests.post(self.url + "/funding/repay", data=json.dumps(payload), auth=self.auth) + #r.raise_for_status() + return r.json() + + def marginTransfer(self, margin_profile_id="", type="",currency="",amount=""): + payload = { + "margin_profile_id": margin_profile_id, + "type": type, + "currency": currency, # example: USD + "amount": amount + } + r = requests.post(self.url + "/profiles/margin-transfer", data=json.dumps(payload), auth=self.auth) + # r.raise_for_status() + return r.json() + + def getPosition(self): + r = requests.get(self.url + "/position", auth=self.auth) + # r.raise_for_status() + return r.json() + + def closePosition(self, repay_only=""): + payload = { + "repay_only": repay_only or False + } + r = requests.post(self.url + "/position/close", data=json.dumps(payload), auth=self.auth) + # r.raise_for_status() + return r.json() + + def deposit(self, amount="", currency="", payment_method_id=""): + payload = { + "amount": amount, + "currency": currency, + "payment_method_id": payment_method_id + } + r = requests.post(self.url + "/deposits/payment-method", data=json.dumps(payload), auth=self.auth) + #r.raise_for_status() + return r.json() + + def coinbaseDeposit(self, amount="", currency="", coinbase_account_id=""): + payload = { + "amount": amount, + "currency": currency, + "coinbase_account_id": coinbase_account_id + } + r = requests.post(self.url + "/deposits/coinbase-account", data=json.dumps(payload), auth=self.auth) + # r.raise_for_status() + return r.json() + + def withdraw(self, amount="", currency="", payment_method_id=""): + payload = { + "amount": amount, + "currency": currency, + "payment_method_id": payment_method_id + } + r = requests.post(self.url + "/withdrawals/payment-method", data=json.dumps(payload), auth=self.auth) + #r.raise_for_status() + return r.json() + + def coinbaseWithdraw(self, amount="", currency="", coinbase_account_id=""): + payload = { + "amount": amount, + "currency": currency, + "coinbase_account_id": coinbase_account_id + } + r = requests.post(self.url + "/withdrawals/coinbase", data=json.dumps(payload), auth=self.auth) + # r.raise_for_status() + return r.json() + + def cryptoWithdraw(self, amount="", currency="", crypto_address=""): + payload = { + "amount": amount, + "currency": currency, + "crypto_address": crypto_address + } + r = requests.post(self.url + "/withdrawals/crypto", data=json.dumps(payload), auth=self.auth) + # r.raise_for_status() + return r.json() + + def getPaymentMethods(self): + r = requests.get(self.url + "/payment-methods", auth=self.auth) + #r.raise_for_status() + return r.json() + + def getCoinbaseAccounts(self): + r = requests.get(self.url + "/coinbase-accounts", auth=self.auth) + #r.raise_for_status() + return r.json() + + def createReport(self, type="", start_date="", end_date="", product_id="", account_id="", format="", email=""): + payload = { + "type": type, + "start_date": start_date, + "end_date": end_date, + "product_id": product_id, + "account_id": account_id, + "format": format, + "email": email + } + r = requests.post(self.url + "/reports", data=json.dumps(payload), auth=self.auth) + #r.raise_for_status() + return r.json() + + def getReport(self, reportId=""): + r = requests.get(self.url + "/reports/" + reportId, auth=self.auth) + #r.raise_for_status() + return r.json() + + def getTrailingVolume(self): + r = requests.get(self.url + "/users/self/trailing-volume", auth=self.auth) + #r.raise_for_status() + return r.json() class GdaxAuth(AuthBase): - # Provided by GDAX: https://docs.gdax.com/#signing-a-message - def __init__(self, api_key, secret_key, passphrase): - self.api_key = api_key - self.secret_key = secret_key - self.passphrase = passphrase - - def __call__(self, request): - timestamp = str(time.time()) - message = timestamp + request.method + request.path_url + (request.body or '') - message = message.encode('ascii') - hmac_key = base64.b64decode(self.secret_key) - signature = hmac.new(hmac_key, message, hashlib.sha256) - signature_b64 = base64.b64encode(signature.digest()) - request.headers.update({ - 'Content-Type': 'Application/JSON', - 'CB-ACCESS-SIGN': signature_b64, - 'CB-ACCESS-TIMESTAMP': timestamp, - 'CB-ACCESS-KEY': self.api_key, - 'CB-ACCESS-PASSPHRASE': self.passphrase - }) - return request + # Provided by GDAX: https://docs.gdax.com/#signing-a-message + def __init__(self, api_key, secret_key, passphrase): + self.api_key = api_key + self.secret_key = secret_key + self.passphrase = passphrase + + def __call__(self, request): + timestamp = str(time.time()) + message = timestamp + request.method + request.path_url + (request.body or '') + message = message.encode('ascii') + hmac_key = base64.b64decode(self.secret_key) + signature = hmac.new(hmac_key, message, hashlib.sha256) + signature_b64 = base64.b64encode(signature.digest()) + request.headers.update({ + 'Content-Type': 'Application/JSON', + 'CB-ACCESS-SIGN': signature_b64, + 'CB-ACCESS-TIMESTAMP': timestamp, + 'CB-ACCESS-KEY': self.api_key, + 'CB-ACCESS-PASSPHRASE': self.passphrase + }) + return request diff --git a/GDAX/OrderBook.py b/GDAX/OrderBook.py index 727829b8..a3ecb9c7 100644 --- a/GDAX/OrderBook.py +++ b/GDAX/OrderBook.py @@ -13,203 +13,203 @@ class OrderBook(WebsocketClient): - def __init__(self, product_id='BTC-USD'): - WebsocketClient.__init__(self, products=product_id) - self._asks = RBTree() - self._bids = RBTree() - self._client = PublicClient(product_id=product_id) - self._sequence = -1 - - def onMessage(self, message): - sequence = message['sequence'] - if self._sequence == -1: - self._asks = RBTree() - self._bids = RBTree() - res = self._client.getProductOrderBook(level=3) - for bid in res['bids']: - self.add({ - 'id': bid[2], - 'side': 'buy', - 'price': float(bid[0]), - 'size': float(bid[1]) - }) - for ask in res['asks']: - self.add({ - 'id': ask[2], - 'side': 'sell', - 'price': float(ask[0]), - 'size': float(ask[1]) - }) - self._sequence = res['sequence'] - - if sequence <= self._sequence or sequence > self._sequence + 1: - print("Out of sequence!", sequence, self._sequence) - self._sequence = sequence - return - - # print(message) - msg_type = message['type'] - if msg_type == 'open': - self.add(message) - elif msg_type == 'done' and 'price' in message: - self.remove(message) - elif msg_type == 'match': - self.match(message) - elif msg_type == 'change': - self.change(message) - - self._sequence = sequence - - # bid = self.get_bid() - # bids = self.get_bids(bid) - # bid_depth = sum([b['size'] for b in bids]) - # ask = self.get_ask() - # asks = self.get_asks(ask) - # ask_depth = sum([a['size'] for a in asks]) - # print('bid: %f @ %f - ask: %f @ %f' % (bid_depth, bid, ask_depth, ask)) - - def add(self, order): - order = { - 'id': order['order_id'] if 'order_id' in order else order['id'], - 'side': order['side'], - 'price': float(order['price']), - 'size': float(order['size']) if 'size' in order else float(order['remaining_size']) - } - if order['side'] == 'buy': - bids = self.get_bids(order['price']) - if bids is None: - bids = [order] - else: - bids.append(order) - self.set_bids(order['price'], bids) - else: - asks = self.get_asks(order['price']) - if asks is None: - asks = [order] - else: - asks.append(order) - self.set_asks(order['price'], asks) - - def remove(self, order): - price = float(order['price']) - if order['side'] == 'buy': - bids = self.get_bids(price) - if bids is not None: - bids = [o for o in bids if o['id'] != order['order_id']] - if len(bids) > 0: - self.set_bids(price, bids) - else: - self.remove_bids(price) - else: - asks = self.get_asks(price) - if asks is not None: - asks = [o for o in asks if o['id'] != order['order_id']] - if len(asks) > 0: - self.set_asks(price, asks) - else: - self.remove_asks(price) - - def match(self, order): - size = float(order['size']) - price = float(order['price']) - - if order['side'] == 'buy': - bids = self.get_bids(price) - if not bids: - return - assert bids[0]['id'] == order['maker_order_id'] - if bids[0]['size'] == size: - self.set_bids(price, bids[1:]) - else: - bids[0]['size'] -= size - self.set_bids(price, bids) - else: - asks = self.get_asks(price) - if not asks: - return - assert asks[0]['id'] == order['maker_order_id'] - if asks[0]['size'] == size: - self.set_asks(price, asks[1:]) - else: - asks[0]['size'] -= size - self.set_asks(price, asks) - - def change(self, order): - new_size = float(order['new_size']) - price = float(order['price']) - - if order['side'] == 'buy': - bids = self.get_bids(price) - if bids is None or not any(o['id'] == order['order_id'] for o in bids): - return - index = map(itemgetter('id'), bids).index(order['order_id']) - bids[index]['size'] = new_size - self.set_bids(price, bids) - else: - asks = self.get_asks(price) - if asks is None or not any(o['id'] == order['order_id'] for o in asks): - return - index = map(itemgetter('id'), asks).index(order['order_id']) - asks[index]['size'] = new_size - self.set_asks(price, asks) - - tree = self._asks if order['side'] == 'sell' else self._bids - node = tree.get(price) - - if node is None or not any(o['id'] == order['order_id'] for o in node): - return - - def get_current_book(self): - result = dict() - result['sequence'] = self._sequence - result['asks'] = list() - result['bids'] = list() - for ask in self._asks: - thisAsk = self._asks[ask] - for order in thisAsk: - result['asks'].append([ - order['price'], - order['size'], - order['id'], - ]) - for bid in self._bids: - thisBid = self._bids[bid] - for order in thisBid: - result['bids'].append([ - order['price'], - order['size'], - order['id'], - ]) - return result - - def get_ask(self): - return self._asks.min_key() - - def get_asks(self, price): - return self._asks.get(price) - - def remove_asks(self, price): - self._asks.remove(price) - - def set_asks(self, price, asks): - self._asks.insert(price, asks) - - def get_bid(self): - return self._bids.max_key() - - def get_bids(self, price): - return self._bids.get(price) - - def remove_bids(self, price): - self._bids.remove(price) - - def set_bids(self, price, bids): - self._bids.insert(price, bids) + def __init__(self, product_id='BTC-USD'): + WebsocketClient.__init__(self, products=product_id) + self._asks = RBTree() + self._bids = RBTree() + self._client = PublicClient(product_id=product_id) + self._sequence = -1 + + def onMessage(self, message): + sequence = message['sequence'] + if self._sequence == -1: + self._asks = RBTree() + self._bids = RBTree() + res = self._client.getProductOrderBook(level=3) + for bid in res['bids']: + self.add({ + 'id': bid[2], + 'side': 'buy', + 'price': float(bid[0]), + 'size': float(bid[1]) + }) + for ask in res['asks']: + self.add({ + 'id': ask[2], + 'side': 'sell', + 'price': float(ask[0]), + 'size': float(ask[1]) + }) + self._sequence = res['sequence'] + + if sequence <= self._sequence or sequence > self._sequence + 1: + print("Out of sequence!", sequence, self._sequence) + self._sequence = sequence + return + + # print(message) + msg_type = message['type'] + if msg_type == 'open': + self.add(message) + elif msg_type == 'done' and 'price' in message: + self.remove(message) + elif msg_type == 'match': + self.match(message) + elif msg_type == 'change': + self.change(message) + + self._sequence = sequence + + # bid = self.get_bid() + # bids = self.get_bids(bid) + # bid_depth = sum([b['size'] for b in bids]) + # ask = self.get_ask() + # asks = self.get_asks(ask) + # ask_depth = sum([a['size'] for a in asks]) + # print('bid: %f @ %f - ask: %f @ %f' % (bid_depth, bid, ask_depth, ask)) + + def add(self, order): + order = { + 'id': order['order_id'] if 'order_id' in order else order['id'], + 'side': order['side'], + 'price': float(order['price']), + 'size': float(order['size']) if 'size' in order else float(order['remaining_size']) + } + if order['side'] == 'buy': + bids = self.get_bids(order['price']) + if bids is None: + bids = [order] + else: + bids.append(order) + self.set_bids(order['price'], bids) + else: + asks = self.get_asks(order['price']) + if asks is None: + asks = [order] + else: + asks.append(order) + self.set_asks(order['price'], asks) + + def remove(self, order): + price = float(order['price']) + if order['side'] == 'buy': + bids = self.get_bids(price) + if bids is not None: + bids = [o for o in bids if o['id'] != order['order_id']] + if len(bids) > 0: + self.set_bids(price, bids) + else: + self.remove_bids(price) + else: + asks = self.get_asks(price) + if asks is not None: + asks = [o for o in asks if o['id'] != order['order_id']] + if len(asks) > 0: + self.set_asks(price, asks) + else: + self.remove_asks(price) + + def match(self, order): + size = float(order['size']) + price = float(order['price']) + + if order['side'] == 'buy': + bids = self.get_bids(price) + if not bids: + return + assert bids[0]['id'] == order['maker_order_id'] + if bids[0]['size'] == size: + self.set_bids(price, bids[1:]) + else: + bids[0]['size'] -= size + self.set_bids(price, bids) + else: + asks = self.get_asks(price) + if not asks: + return + assert asks[0]['id'] == order['maker_order_id'] + if asks[0]['size'] == size: + self.set_asks(price, asks[1:]) + else: + asks[0]['size'] -= size + self.set_asks(price, asks) + + def change(self, order): + new_size = float(order['new_size']) + price = float(order['price']) + + if order['side'] == 'buy': + bids = self.get_bids(price) + if bids is None or not any(o['id'] == order['order_id'] for o in bids): + return + index = map(itemgetter('id'), bids).index(order['order_id']) + bids[index]['size'] = new_size + self.set_bids(price, bids) + else: + asks = self.get_asks(price) + if asks is None or not any(o['id'] == order['order_id'] for o in asks): + return + index = map(itemgetter('id'), asks).index(order['order_id']) + asks[index]['size'] = new_size + self.set_asks(price, asks) + + tree = self._asks if order['side'] == 'sell' else self._bids + node = tree.get(price) + + if node is None or not any(o['id'] == order['order_id'] for o in node): + return + + def get_current_book(self): + result = dict() + result['sequence'] = self._sequence + result['asks'] = list() + result['bids'] = list() + for ask in self._asks: + thisAsk = self._asks[ask] + for order in thisAsk: + result['asks'].append([ + order['price'], + order['size'], + order['id'], + ]) + for bid in self._bids: + thisBid = self._bids[bid] + for order in thisBid: + result['bids'].append([ + order['price'], + order['size'], + order['id'], + ]) + return result + + def get_ask(self): + return self._asks.min_key() + + def get_asks(self, price): + return self._asks.get(price) + + def remove_asks(self, price): + self._asks.remove(price) + + def set_asks(self, price, asks): + self._asks.insert(price, asks) + + def get_bid(self): + return self._bids.max_key() + + def get_bids(self, price): + return self._bids.get(price) + + def remove_bids(self, price): + self._bids.remove(price) + + def set_bids(self, price, bids): + self._bids.insert(price, bids) if __name__ == '__main__': - import time - order_book = OrderBook() - order_book.start() - time.sleep(10) - order_book.close() + import time + order_book = OrderBook() + order_book.start() + time.sleep(10) + order_book.close() diff --git a/GDAX/PublicClient.py b/GDAX/PublicClient.py index d98dc72a..fda7f8a9 100644 --- a/GDAX/PublicClient.py +++ b/GDAX/PublicClient.py @@ -7,65 +7,65 @@ import requests class PublicClient(): - def __init__(self, api_url="https://api.gdax.com", product_id="BTC-USD"): - self.url = api_url - if api_url[-1] == "/": - self.url = api_url[:-1] - self.productId = product_id + def __init__(self, api_url="https://api.gdax.com", product_id="BTC-USD"): + self.url = api_url + if api_url[-1] == "/": + self.url = api_url[:-1] + self.productId = product_id - def getProducts(self): - r = requests.get(self.url + '/products') - #r.raise_for_status() - return r.json() + def getProducts(self): + r = requests.get(self.url + '/products') + #r.raise_for_status() + return r.json() - def getProductOrderBook(self, json=None, level=2, product=''): - if type(json) is dict: - if "product" in json: product = json["product"] - if "level" in json: level = json['level'] - r = requests.get(self.url + '/products/%s/book?level=%s' % (product or self.productId, str(level))) - #r.raise_for_status() - return r.json() + def getProductOrderBook(self, json=None, level=2, product=''): + if type(json) is dict: + if "product" in json: product = json["product"] + if "level" in json: level = json['level'] + r = requests.get(self.url + '/products/%s/book?level=%s' % (product or self.productId, str(level))) + #r.raise_for_status() + return r.json() - def getProductTicker(self, json=None, product=''): - if type(json) is dict: - if "product" in json: product = json["product"] - r = requests.get(self.url + '/products/%s/ticker' % (product or self.productId)) - #r.raise_for_status() - return r.json() + def getProductTicker(self, json=None, product=''): + if type(json) is dict: + if "product" in json: product = json["product"] + r = requests.get(self.url + '/products/%s/ticker' % (product or self.productId)) + #r.raise_for_status() + return r.json() - def getProductTrades(self, json=None, product=''): - if type(json) is dict: - if "product" in json: product = json["product"] - r = requests.get(self.url + '/products/%s/trades' % (product or self.productId)) - #r.raise_for_status() - return r.json() + def getProductTrades(self, json=None, product=''): + if type(json) is dict: + if "product" in json: product = json["product"] + r = requests.get(self.url + '/products/%s/trades' % (product or self.productId)) + #r.raise_for_status() + return r.json() - def getProductHistoricRates(self, json=None, product='', start='', end='', granularity=''): - payload = {} - if type(json) is dict: - if "product" in json: product = json["product"] - payload = json - else: - payload["start"] = start - payload["end"] = end - payload["granularity"] = granularity - r = requests.get(self.url + '/products/%s/candles' % (product or self.productId), params=payload) - #r.raise_for_status() - return r.json() + def getProductHistoricRates(self, json=None, product='', start='', end='', granularity=''): + payload = {} + if type(json) is dict: + if "product" in json: product = json["product"] + payload = json + else: + payload["start"] = start + payload["end"] = end + payload["granularity"] = granularity + r = requests.get(self.url + '/products/%s/candles' % (product or self.productId), params=payload) + #r.raise_for_status() + return r.json() - def getProduct24HrStats(self, json=None, product=''): - if type(json) is dict: - if "product" in json: product = json["product"] - r = requests.get(self.url + '/products/%s/stats' % (product or self.productId)) - #r.raise_for_status() - return r.json() + def getProduct24HrStats(self, json=None, product=''): + if type(json) is dict: + if "product" in json: product = json["product"] + r = requests.get(self.url + '/products/%s/stats' % (product or self.productId)) + #r.raise_for_status() + return r.json() - def getCurrencies(self): - r = requests.get(self.url + '/currencies') - #r.raise_for_status() - return r.json() + def getCurrencies(self): + r = requests.get(self.url + '/currencies') + #r.raise_for_status() + return r.json() - def getTime(self): - r = requests.get(self.url + '/time') - #r.raise_for_status() - return r.json() + def getTime(self): + r = requests.get(self.url + '/time') + #r.raise_for_status() + return r.json() diff --git a/GDAX/WebsocketClient.py b/GDAX/WebsocketClient.py index 788e395c..b048f15e 100644 --- a/GDAX/WebsocketClient.py +++ b/GDAX/WebsocketClient.py @@ -11,96 +11,96 @@ from websocket import create_connection class WebsocketClient(object): - def __init__(self, url=None, products=None, type=None): - if url is None: - url = "wss://ws-feed.gdax.com" - - self.url = url - self.products = products - self.type = "subscribe" #type or "subscribe" - self.stop = False - self.ws = None - self.thread = None - - def start(self): - def _go(): - self._connect() - self._listen() - - self.onOpen() - self.ws = create_connection(self.url) - self.thread = Thread(target=_go) - self.thread.start() - - def _connect(self): - if self.products is None: - self.products = ["BTC-USD"] - elif not isinstance(self.products, list): - self.products = [self.products] - - if self.url[-1] == "/": - self.url = self.url[:-1] - - self.stop = False - sub_params = {'type': 'subscribe', 'product_ids': self.products} - self.ws.send(json.dumps(sub_params)) - if self.type == "heartbeat": - sub_params = {"type": "heartbeat", "on": True} - self.ws.send(json.dumps(sub_params)) - - def _listen(self): - while not self.stop: - try: - msg = json.loads(self.ws.recv()) - except Exception as e: - self.onError(e) - self.close() - else: - self.onMessage(msg) - - def close(self): - if not self.stop: - if self.type == "heartbeat": - self.ws.send(json.dumps({"type": "heartbeat", "on": False})) - self.onClose() - - def onOpen(self): - print("-- Subscribed! --\n") - - def onClose(self): - self.stop = True - #self.thread = None - self.ws.close() - print("\n-- Socket Closed --") - - def onMessage(self, msg): - print(msg) - - def onError(self, e): - SystemError(e) + def __init__(self, url=None, products=None, type=None): + if url is None: + url = "wss://ws-feed.gdax.com" + + self.url = url + self.products = products + self.type = "subscribe" #type or "subscribe" + self.stop = False + self.ws = None + self.thread = None + + def start(self): + def _go(): + self._connect() + self._listen() + + self.onOpen() + self.ws = create_connection(self.url) + self.thread = Thread(target=_go) + self.thread.start() + + def _connect(self): + if self.products is None: + self.products = ["BTC-USD"] + elif not isinstance(self.products, list): + self.products = [self.products] + + if self.url[-1] == "/": + self.url = self.url[:-1] + + self.stop = False + sub_params = {'type': 'subscribe', 'product_ids': self.products} + self.ws.send(json.dumps(sub_params)) + if self.type == "heartbeat": + sub_params = {"type": "heartbeat", "on": True} + self.ws.send(json.dumps(sub_params)) + + def _listen(self): + while not self.stop: + try: + msg = json.loads(self.ws.recv()) + except Exception as e: + self.onError(e) + self.close() + else: + self.onMessage(msg) + + def close(self): + if not self.stop: + if self.type == "heartbeat": + self.ws.send(json.dumps({"type": "heartbeat", "on": False})) + self.onClose() + + def onOpen(self): + print("-- Subscribed! --\n") + + def onClose(self): + self.stop = True + #self.thread = None + self.ws.close() + print("\n-- Socket Closed --") + + def onMessage(self, msg): + print(msg) + + def onError(self, e): + SystemError(e) if __name__ == "__main__": - import GDAX, time - class myWebsocketClient(GDAX.WebsocketClient): - def onOpen(self): - self.url = "wss://ws-feed.gdax.com/" - self.products = ["BTC-USD", "ETH-USD"] - self.MessageCount = 0 - print ("Lets count the messages!") - - def onMessage(self, msg): - print ("Message type:", msg["type"], "\t@ %.3f" % float(msg["price"])) - self.MessageCount += 1 - - def onClose(self): - print ("-- Goodbye! --") - - wsClient = myWebsocketClient() - wsClient.start() - print(wsClient.url, wsClient.products) - # Do some logic with the data - while (wsClient.MessageCount < 500): - print ("\nMessageCount =", "%i \n") % wsClient.MessageCount - time.sleep(1) - wsClient.close() + import GDAX, time + class myWebsocketClient(GDAX.WebsocketClient): + def onOpen(self): + self.url = "wss://ws-feed.gdax.com/" + self.products = ["BTC-USD", "ETH-USD"] + self.MessageCount = 0 + print ("Lets count the messages!") + + def onMessage(self, msg): + print ("Message type:", msg["type"], "\t@ %.3f" % float(msg["price"])) + self.MessageCount += 1 + + def onClose(self): + print ("-- Goodbye! --") + + wsClient = myWebsocketClient() + wsClient.start() + print(wsClient.url, wsClient.products) + # Do some logic with the data + while (wsClient.MessageCount < 500): + print ("\nMessageCount =", "%i \n") % wsClient.MessageCount + time.sleep(1) + wsClient.close() From 4b9f95716d4e6dced1054e70998a26f8bb878d89 Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Sun, 21 May 2017 21:15:35 -0400 Subject: [PATCH 004/174] look great ...except for the changes to .onClose() -- This is to be overwritten by the developer so we should not include necessary code there. --- GDAX/WebsocketClient.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/GDAX/WebsocketClient.py b/GDAX/WebsocketClient.py index b048f15e..ed031eae 100644 --- a/GDAX/WebsocketClient.py +++ b/GDAX/WebsocketClient.py @@ -63,14 +63,14 @@ def close(self): if self.type == "heartbeat": self.ws.send(json.dumps({"type": "heartbeat", "on": False})) self.onClose() + self.stop = True + #self.thread = None + self.ws.close() def onOpen(self): print("-- Subscribed! --\n") def onClose(self): - self.stop = True - #self.thread = None - self.ws.close() print("\n-- Socket Closed --") def onMessage(self, msg): From 46ca98271389e1dfb2e6e85fa4e63e4c90219e90 Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Sun, 21 May 2017 21:24:41 -0400 Subject: [PATCH 005/174] Reverted out-of-sequence logic --- GDAX/OrderBook.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/GDAX/OrderBook.py b/GDAX/OrderBook.py index a3ecb9c7..d17b5058 100644 --- a/GDAX/OrderBook.py +++ b/GDAX/OrderBook.py @@ -42,9 +42,11 @@ def onMessage(self, message): }) self._sequence = res['sequence'] - if sequence <= self._sequence or sequence > self._sequence + 1: - print("Out of sequence!", sequence, self._sequence) - self._sequence = sequence + if sequence <= self._sequence: + return #ignore old messages + elif sequence > self._sequence + 1: + self.close() + self.start() return # print(message) From b9be198ddc75a13f5989472d200c196c1e6cdd41 Mon Sep 17 00:00:00 2001 From: uzbit Date: Mon, 22 May 2017 07:48:29 -0700 Subject: [PATCH 006/174] Pulling racecondition fix --- GDAX/OrderBook.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/GDAX/OrderBook.py b/GDAX/OrderBook.py index d17b5058..9c27c14d 100644 --- a/GDAX/OrderBook.py +++ b/GDAX/OrderBook.py @@ -167,7 +167,12 @@ def get_current_book(self): result['asks'] = list() result['bids'] = list() for ask in self._asks: - thisAsk = self._asks[ask] + try: + # There can be a race condition here, where a price point is removed + # between these two ops + thisAsk = self._asks[ask] + except KeyError: + continue for order in thisAsk: result['asks'].append([ order['price'], @@ -175,7 +180,13 @@ def get_current_book(self): order['id'], ]) for bid in self._bids: - thisBid = self._bids[bid] + try: + # There can be a race condition here, where a price point is removed + # between these two ops + thisBid = self._bids[bid] + except KeyError: + continue + for order in thisBid: result['bids'].append([ order['price'], From d8237743e30d193c353070fb227579f4c682c38c Mon Sep 17 00:00:00 2001 From: dj Date: Fri, 26 May 2017 10:47:31 -0400 Subject: [PATCH 007/174] fixed type error with print formatter --- GDAX/WebsocketClient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GDAX/WebsocketClient.py b/GDAX/WebsocketClient.py index ed031eae..9dfb7a86 100644 --- a/GDAX/WebsocketClient.py +++ b/GDAX/WebsocketClient.py @@ -101,6 +101,6 @@ def onClose(self): print(wsClient.url, wsClient.products) # Do some logic with the data while (wsClient.MessageCount < 500): - print ("\nMessageCount =", "%i \n") % wsClient.MessageCount + print("\nMessageCount =", "%i \n" % wsClient.MessageCount) time.sleep(1) wsClient.close() From 614deb85ae45d272a390d53bd4fecad9d2bea43a Mon Sep 17 00:00:00 2001 From: Dan Paquin Date: Fri, 26 May 2017 20:54:32 -0400 Subject: [PATCH 008/174] Updated Websocket and README for v0.3 --- GDAX/WebsocketClient.py | 3 ++- README.md | 25 ++++++++++++++++--------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/GDAX/WebsocketClient.py b/GDAX/WebsocketClient.py index ed031eae..58745e3a 100644 --- a/GDAX/WebsocketClient.py +++ b/GDAX/WebsocketClient.py @@ -65,6 +65,7 @@ def close(self): self.onClose() self.stop = True #self.thread = None + self.thread.join() self.ws.close() def onOpen(self): @@ -101,6 +102,6 @@ def onClose(self): print(wsClient.url, wsClient.products) # Do some logic with the data while (wsClient.MessageCount < 500): - print ("\nMessageCount =", "%i \n") % wsClient.MessageCount + print ("\nMessageCount =", "%i \n" % wsClient.MessageCount) time.sleep(1) wsClient.close() diff --git a/README.md b/README.md index 47bd22cc..76bcb4bc 100644 --- a/README.md +++ b/README.md @@ -250,19 +250,19 @@ import GDAX, time class myWebsocketClient(GDAX.WebsocketClient): def onOpen(self): self.url = "wss://ws-feed.gdax.com/" - self.products = ["BTC-USD", "ETH-USD"] + self.products = ["LTC-USD"] self.MessageCount = 0 print("Lets count the messages!") def onMessage(self, msg): - print("Message type:", msg["type"], "\t@ %.3f" % float(msg["price"])) self.MessageCount += 1 + if 'price' in msg and 'type' in msg: + print("Message type:", msg["type"], "\t@ %.3f" % float(msg["price"])) def onClose(self): print("-- Goodbye! --") wsClient = myWebsocketClient() wsClient.start() print(wsClient.url, wsClient.products) -# Do some logic with the data while (wsClient.MessageCount < 500): print("\nMessageCount =", "%i \n" % wsClient.MessageCount) time.sleep(1) @@ -281,7 +281,14 @@ order_book.close() ``` ## Change Log -*0.2.2* **Current PyPI release** +*0.3* **Current PyPI release** +- Added crypto and LTC deposit & withdraw (undocumented). +- Added support for Margin trading (undocumented). +- Enhanced functionality of the WebsocketClient. +- Soft launch of the OrderBook (undocumented). +- Minor bug squashing & syntax improvements. + +*0.2.2* - Added additional API functionality such as cancelAll() and ETH withdrawal. *0.2.1* @@ -289,12 +296,12 @@ order_book.close() *0.2.0* - Renamed project to GDAX-Python -- Merged Websocket updates to handle errors and reconnect +- Merged Websocket updates to handle errors and reconnect. *0.1.2* -- Updated JSON handling for increased compatibility among some users -- Added support for payment methods, reports, and coinbase user accounts -- Other compatibility updates +- Updated JSON handling for increased compatibility among some users. +- Added support for payment methods, reports, and coinbase user accounts. +- Other compatibility updates. *0.1.1b2* -- Original PyPI Release +- Original PyPI Release. From 7edbcfba12b49ca714e3a39779b324634ed03707 Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Sat, 27 May 2017 13:36:45 -0400 Subject: [PATCH 009/174] Cleared conflict with WebsocketClient --- GDAX/WebsocketClient.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/GDAX/WebsocketClient.py b/GDAX/WebsocketClient.py index 898a113f..58745e3a 100644 --- a/GDAX/WebsocketClient.py +++ b/GDAX/WebsocketClient.py @@ -102,10 +102,6 @@ def onClose(self): print(wsClient.url, wsClient.products) # Do some logic with the data while (wsClient.MessageCount < 500): -<<<<<<< HEAD print ("\nMessageCount =", "%i \n" % wsClient.MessageCount) -======= - print("\nMessageCount =", "%i \n" % wsClient.MessageCount) ->>>>>>> e5d2bb8d930db1ddf64ab446467845a41f0ab5cd time.sleep(1) wsClient.close() From 25d8ba04d2a565a69c16380ebe70abc916ba439f Mon Sep 17 00:00:00 2001 From: Dan Paquin Date: Mon, 29 May 2017 21:15:39 -0400 Subject: [PATCH 010/174] fixed syntax error in WebsocketClient --- GDAX/WebsocketClient.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/GDAX/WebsocketClient.py b/GDAX/WebsocketClient.py index 58745e3a..da3c0190 100644 --- a/GDAX/WebsocketClient.py +++ b/GDAX/WebsocketClient.py @@ -91,7 +91,8 @@ def onOpen(self): print ("Lets count the messages!") def onMessage(self, msg): - print ("Message type:", msg["type"], "\t@ %.3f" % float(msg["price"])) + if 'price' in msg and 'type' in msg: + print ("Message type:", msg["type"], "\t@ %.3f" % float(msg["price"])) self.MessageCount += 1 def onClose(self): From 5ce1dafd539286b542a142f6f8245993b522d534 Mon Sep 17 00:00:00 2001 From: Dan Paquin Date: Mon, 29 May 2017 21:17:52 -0400 Subject: [PATCH 011/174] setup v0.3.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 738028a3..ebc11347 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name = 'GDAX', - version = '0.2.2', + version = '0.3.1', author = 'Daniel Paquin', author_email = 'dpaq34@gmail.com', license='MIT', From 458fa865b8a84bba6b468d3a3d0c861a3e9b56c1 Mon Sep 17 00:00:00 2001 From: Bryan Kaplan <#@bryankaplan.com> Date: Mon, 29 May 2017 04:14:36 -0700 Subject: [PATCH 012/174] Avoid binary floating point arithmetic --- GDAX/OrderBook.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/GDAX/OrderBook.py b/GDAX/OrderBook.py index 9c27c14d..e6a15957 100644 --- a/GDAX/OrderBook.py +++ b/GDAX/OrderBook.py @@ -6,6 +6,7 @@ from operator import itemgetter from bintrees import RBTree +from decimal import Decimal from GDAX.PublicClient import PublicClient from GDAX.WebsocketClient import WebsocketClient @@ -30,15 +31,15 @@ def onMessage(self, message): self.add({ 'id': bid[2], 'side': 'buy', - 'price': float(bid[0]), - 'size': float(bid[1]) + 'price': Decimal(bid[0]), + 'size': Decimal(bid[1]) }) for ask in res['asks']: self.add({ 'id': ask[2], 'side': 'sell', - 'price': float(ask[0]), - 'size': float(ask[1]) + 'price': Decimal(ask[0]), + 'size': Decimal(ask[1]) }) self._sequence = res['sequence'] @@ -74,8 +75,8 @@ def add(self, order): order = { 'id': order['order_id'] if 'order_id' in order else order['id'], 'side': order['side'], - 'price': float(order['price']), - 'size': float(order['size']) if 'size' in order else float(order['remaining_size']) + 'price': Decimal(order['price']), + 'size': Decimal(order.get('size', order['remaining_size'])) } if order['side'] == 'buy': bids = self.get_bids(order['price']) @@ -93,7 +94,7 @@ def add(self, order): self.set_asks(order['price'], asks) def remove(self, order): - price = float(order['price']) + price = Decimal(order['price']) if order['side'] == 'buy': bids = self.get_bids(price) if bids is not None: @@ -112,8 +113,8 @@ def remove(self, order): self.remove_asks(price) def match(self, order): - size = float(order['size']) - price = float(order['price']) + size = Decimal(order['size']) + price = Decimal(order['price']) if order['side'] == 'buy': bids = self.get_bids(price) @@ -137,8 +138,8 @@ def match(self, order): self.set_asks(price, asks) def change(self, order): - new_size = float(order['new_size']) - price = float(order['price']) + new_size = Decimal(order['new_size']) + price = Decimal(order['price']) if order['side'] == 'buy': bids = self.get_bids(price) From 6a222e945d10e7a10ccaab6dc60f6e61f93697e0 Mon Sep 17 00:00:00 2001 From: Bryan Kaplan <#@bryankaplan.com> Date: Mon, 29 May 2017 12:06:29 -0700 Subject: [PATCH 013/174] Accept kwargs in buy and sell Previously buy and sell accepted a single dict of arbitrary parameters. That worked, but it wasn't very pythonic. This commit changes them both to accept arbitrary kwargs. The functionality is the same, but now it looks like python. I have also taken the opportunity to format the lines I altered in accordance with PEP-8. --- GDAX/AuthenticatedClient.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/GDAX/AuthenticatedClient.py b/GDAX/AuthenticatedClient.py index 4aa2a027..360f049e 100644 --- a/GDAX/AuthenticatedClient.py +++ b/GDAX/AuthenticatedClient.py @@ -60,18 +60,20 @@ def holdsPagination(self, accountId, list, after): self.holdsPagination(accountId, list, r.headers["cb-after"]) return list - def buy(self, buyParams): - buyParams["side"] = "buy" - if not buyParams["product_id"]: - buyParams["product_id"] = self.productId - r = requests.post(self.url + '/orders', data=json.dumps(buyParams), auth=self.auth) - #r.raise_for_status() + def buy(self, **kwargs): + kwargs["side"] = "buy" + if not "product_id" in kwargs: + kwargs["product_id"] = self.productId + r = requests.post(self.url + '/orders', + data=json.dumps(kwargs), + auth=self.auth) return r.json() - def sell(self, sellParams): - sellParams["side"] = "sell" - r = requests.post(self.url + '/orders', data=json.dumps(sellParams), auth=self.auth) - #r.raise_for_status() + def sell(self, **kwargs): + kwargs["side"] = "sell" + r = requests.post(self.url + '/orders', + data=json.dumps(kwargs), + auth=self.auth) return r.json() def cancelOrder(self, orderId): From c64622a74b8f32bbfb41d947cc2af7aaca9dbefd Mon Sep 17 00:00:00 2001 From: dj Date: Tue, 30 May 2017 11:09:02 -0400 Subject: [PATCH 014/174] Replaced % substitutions with format method --- GDAX/AuthenticatedClient.py | 30 +++++++++++++++--------------- GDAX/PublicClient.py | 10 +++++----- GDAX/WebsocketClient.py | 5 +++-- README.md | 4 ++-- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/GDAX/AuthenticatedClient.py b/GDAX/AuthenticatedClient.py index 4aa2a027..aa1ad9e3 100644 --- a/GDAX/AuthenticatedClient.py +++ b/GDAX/AuthenticatedClient.py @@ -26,7 +26,7 @@ def getAccounts(self): def getAccountHistory(self, accountId): list = [] - r = requests.get(self.url + '/accounts/%s/ledger' %accountId, auth=self.auth) + r = requests.get(self.url + '/accounts/{}/ledger'.format(accountId), auth=self.auth) #r.raise_for_status() list.append(r.json()) if "cb-after" in r.headers: @@ -34,7 +34,7 @@ def getAccountHistory(self, accountId): return list def historyPagination(self, accountId, list, after): - r = requests.get(self.url + '/accounts/%s/ledger?after=%s' %(accountId, str(after)), auth=self.auth) + r = requests.get(self.url + '/accounts/{}/ledger?after={}'.format(accountId, str(after)), auth=self.auth) #r.raise_for_status() if r.json(): list.append(r.json()) @@ -44,7 +44,7 @@ def historyPagination(self, accountId, list, after): def getAccountHolds(self, accountId): list = [] - r = requests.get(self.url + '/accounts/%s/holds' %accountId, auth=self.auth) + r = requests.get(self.url + '/accounts/{}/holds'.format(accountId), auth=self.auth) #r.raise_for_status() list.append(r.json()) if "cb-after" in r.headers: @@ -52,7 +52,7 @@ def getAccountHolds(self, accountId): return list def holdsPagination(self, accountId, list, after): - r = requests.get(self.url + '/accounts/%s/holds?after=%s' %(accountId, str(after)), auth=self.auth) + r = requests.get(self.url + '/accounts/{}/holds?after={}'.format(accountId, str(after)), auth=self.auth) #r.raise_for_status() if r.json(): list.append(r.json()) @@ -101,7 +101,7 @@ def getOrders(self): return list def paginateOrders(self, list, after): - r = requests.get(self.url + '/orders?after=%s' %str(after)) + r = requests.get(self.url + '/orders?after={}'.format(str(after))) #r.raise_for_status() if r.json(): list.append(r.json()) @@ -112,11 +112,11 @@ def paginateOrders(self, list, after): def getFills(self, orderId='', productId='', before='', after='', limit=''): list = [] url = self.url + '/fills?' - if orderId: url += "order_id=%s&" %str(orderId) - if productId: url += "product_id=%s&" %(productId or self.productId) - if before: url += "before=%s&" %str(before) - if after: url += "after=%s&" %str(after) - if limit: url += "limit=%s&" %str(limit) + if orderId: url += "order_id={}&".format(str(orderId)) + if productId: url += "product_id={}&".format(productId or self.productId) + if before: url += "before={}&".format(str(before)) + if after: url += "after={}&".format(str(after)) + if limit: url += "limit={}&".format(str(limit)) r = requests.get(url, auth=self.auth) #r.raise_for_status() list.append(r.json()) @@ -125,9 +125,9 @@ def getFills(self, orderId='', productId='', before='', after='', limit=''): return list def paginateFills(self, list, after, orderId='', productId=''): - url = self.url + '/fills?after=%s&' % str(after) - if orderId: url += "order_id=%s&" % str(orderId) - if productId: url += "product_id=%s&" % (productId or self.productId) + url = self.url + '/fills?after={}&'.format(str(after)) + if orderId: url += "order_id={}&".format(str(orderId)) + if productId: url += "product_id={}&".format(productId or self.productId) r = requests.get(url, auth=self.auth) #r.raise_for_status() if r.json(): @@ -139,8 +139,8 @@ def paginateFills(self, list, after, orderId='', productId=''): def getFundings(self, list='', status='', after=''): if not list: list = [] url = self.url + '/funding?' - if status: url += "status=%s&" % str(status) - if after: url += 'after=%s&' % str(after) + if status: url += "status={}&".format(str(status)) + if after: url += 'after={}&'.format(str(after)) r = requests.get(url, auth=self.auth) #r.raise_for_status() list.append(r.json()) diff --git a/GDAX/PublicClient.py b/GDAX/PublicClient.py index fda7f8a9..b167263e 100644 --- a/GDAX/PublicClient.py +++ b/GDAX/PublicClient.py @@ -22,21 +22,21 @@ def getProductOrderBook(self, json=None, level=2, product=''): if type(json) is dict: if "product" in json: product = json["product"] if "level" in json: level = json['level'] - r = requests.get(self.url + '/products/%s/book?level=%s' % (product or self.productId, str(level))) + r = requests.get(self.url + '/products/{}/book?level={}'.format(product or self.productId, str(level))) #r.raise_for_status() return r.json() def getProductTicker(self, json=None, product=''): if type(json) is dict: if "product" in json: product = json["product"] - r = requests.get(self.url + '/products/%s/ticker' % (product or self.productId)) + r = requests.get(self.url + '/products/{}/ticker'.format(product or self.productId)) #r.raise_for_status() return r.json() def getProductTrades(self, json=None, product=''): if type(json) is dict: if "product" in json: product = json["product"] - r = requests.get(self.url + '/products/%s/trades' % (product or self.productId)) + r = requests.get(self.url + '/products/{}/trades'.format(product or self.productId)) #r.raise_for_status() return r.json() @@ -49,14 +49,14 @@ def getProductHistoricRates(self, json=None, product='', start='', end='', granu payload["start"] = start payload["end"] = end payload["granularity"] = granularity - r = requests.get(self.url + '/products/%s/candles' % (product or self.productId), params=payload) + r = requests.get(self.url + '/products/{}/candles'.format(product or self.productId), params=payload) #r.raise_for_status() return r.json() def getProduct24HrStats(self, json=None, product=''): if type(json) is dict: if "product" in json: product = json["product"] - r = requests.get(self.url + '/products/%s/stats' % (product or self.productId)) + r = requests.get(self.url + '/products/{}/stats'.format(product or self.productId)) #r.raise_for_status() return r.json() diff --git a/GDAX/WebsocketClient.py b/GDAX/WebsocketClient.py index da3c0190..d40930a4 100644 --- a/GDAX/WebsocketClient.py +++ b/GDAX/WebsocketClient.py @@ -92,7 +92,7 @@ def onOpen(self): def onMessage(self, msg): if 'price' in msg and 'type' in msg: - print ("Message type:", msg["type"], "\t@ %.3f" % float(msg["price"])) + print ("Message type:", msg["type"], "\t@ {}.3f".format(float(msg["price"]))) self.MessageCount += 1 def onClose(self): @@ -103,6 +103,7 @@ def onClose(self): print(wsClient.url, wsClient.products) # Do some logic with the data while (wsClient.MessageCount < 500): - print ("\nMessageCount =", "%i \n" % wsClient.MessageCount) + print ("\nMessageCount =", "{} \n".format(wsClient.MessageCount)) time.sleep(1) + wsClient.close() diff --git a/README.md b/README.md index 76bcb4bc..b9a24312 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ class myWebsocketClient(GDAX.WebsocketClient): def onMessage(self, msg): self.MessageCount += 1 if 'price' in msg and 'type' in msg: - print("Message type:", msg["type"], "\t@ %.3f" % float(msg["price"])) + print ("Message type:", msg["type"], "\t@ {}.3f".format(float(msg["price"]))) def onClose(self): print("-- Goodbye! --") @@ -264,7 +264,7 @@ wsClient = myWebsocketClient() wsClient.start() print(wsClient.url, wsClient.products) while (wsClient.MessageCount < 500): - print("\nMessageCount =", "%i \n" % wsClient.MessageCount) + print ("\nMessageCount =", "{} \n".format(wsClient.MessageCount)) time.sleep(1) wsClient.close() ``` From 2a6558a6ca34857ad2dccb1c6769eb45abfca15d Mon Sep 17 00:00:00 2001 From: Ted McCormack Date: Tue, 30 May 2017 20:35:19 -0700 Subject: [PATCH 015/174] Stash --- GDAX/OrderBook.py | 22 +++++++++++++++++++--- GDAX/WebsocketClient.py | 6 +++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/GDAX/OrderBook.py b/GDAX/OrderBook.py index 9c27c14d..3d642872 100644 --- a/GDAX/OrderBook.py +++ b/GDAX/OrderBook.py @@ -6,6 +6,7 @@ from operator import itemgetter from bintrees import RBTree +from collections import deque from GDAX.PublicClient import PublicClient from GDAX.WebsocketClient import WebsocketClient @@ -13,14 +14,19 @@ class OrderBook(WebsocketClient): - def __init__(self, product_id='BTC-USD'): + def __init__(self, product_id='BTC-USD', log=False): WebsocketClient.__init__(self, products=product_id) self._asks = RBTree() self._bids = RBTree() + self._deque = deque() self._client = PublicClient(product_id=product_id) self._sequence = -1 + self._log = log def onMessage(self, message): + if self._log: + self._deque.append(message) + sequence = message['sequence'] if self._sequence == -1: self._asks = RBTree() @@ -45,8 +51,7 @@ def onMessage(self, message): if sequence <= self._sequence: return #ignore old messages elif sequence > self._sequence + 1: - self.close() - self.start() + self.reset() return # print(message) @@ -69,6 +74,11 @@ def onMessage(self, message): # asks = self.get_asks(ask) # ask_depth = sum([a['size'] for a in asks]) # print('bid: %f @ %f - ask: %f @ %f' % (bid_depth, bid, ask_depth, ask)) + def reset(self): + print("Restarting thread.") + self.close() + self.start() + self._sequence = -1 def add(self, order): order = { @@ -195,6 +205,12 @@ def get_current_book(self): ]) return result + def get_deque(self): + return self._deque + + def clear_deque(self): + self._deque.clear() + def get_ask(self): return self._asks.min_key() diff --git a/GDAX/WebsocketClient.py b/GDAX/WebsocketClient.py index ed031eae..432635ed 100644 --- a/GDAX/WebsocketClient.py +++ b/GDAX/WebsocketClient.py @@ -11,13 +11,14 @@ from websocket import create_connection class WebsocketClient(object): + # Need to make a server to test. def __init__(self, url=None, products=None, type=None): if url is None: url = "wss://ws-feed.gdax.com" self.url = url self.products = products - self.type = "subscribe" #type or "subscribe" + self.type = type or "subscribe" self.stop = False self.ws = None self.thread = None @@ -28,7 +29,6 @@ def _go(): self._listen() self.onOpen() - self.ws = create_connection(self.url) self.thread = Thread(target=_go) self.thread.start() @@ -42,6 +42,7 @@ def _connect(self): self.url = self.url[:-1] self.stop = False + self.ws = create_connection(self.url) sub_params = {'type': 'subscribe', 'product_ids': self.products} self.ws.send(json.dumps(sub_params)) if self.type == "heartbeat": @@ -64,7 +65,6 @@ def close(self): self.ws.send(json.dumps({"type": "heartbeat", "on": False})) self.onClose() self.stop = True - #self.thread = None self.ws.close() def onOpen(self): From 8d100915a452ca9bd6ccaa0fcc0e4088ca18d9ed Mon Sep 17 00:00:00 2001 From: Ted McCormack Date: Tue, 30 May 2017 20:51:26 -0700 Subject: [PATCH 016/174] Stash --- GDAX/OrderBook.py | 41 ++++++++++++++++++----------------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/GDAX/OrderBook.py b/GDAX/OrderBook.py index 83758158..1a0f75c5 100644 --- a/GDAX/OrderBook.py +++ b/GDAX/OrderBook.py @@ -6,7 +6,7 @@ from operator import itemgetter from bintrees import RBTree -from decimal import Decimal +import pickle from GDAX.PublicClient import PublicClient from GDAX.WebsocketClient import WebsocketClient @@ -14,18 +14,19 @@ class OrderBook(WebsocketClient): - def __init__(self, product_id='BTC-USD', log=False): + def __init__(self, product_id='BTC-USD', logTo=None): WebsocketClient.__init__(self, products=product_id) self._asks = RBTree() self._bids = RBTree() - self._deque = deque() self._client = PublicClient(product_id=product_id) self._sequence = -1 - self._log = log + if logTo: + assert hasattr(logTo, 'write') + self._logTo = logTo def onMessage(self, message): - if self._log: - self._deque.append(message) + if self._logTo: + pickle.dump(message, self._logTo) sequence = message['sequence'] if self._sequence == -1: @@ -36,15 +37,15 @@ def onMessage(self, message): self.add({ 'id': bid[2], 'side': 'buy', - 'price': Decimal(bid[0]), - 'size': Decimal(bid[1]) + 'price': float(bid[0]), + 'size': float(bid[1]) }) for ask in res['asks']: self.add({ 'id': ask[2], 'side': 'sell', - 'price': Decimal(ask[0]), - 'size': Decimal(ask[1]) + 'price': float(ask[0]), + 'size': float(ask[1]) }) self._sequence = res['sequence'] @@ -84,8 +85,8 @@ def add(self, order): order = { 'id': order['order_id'] if 'order_id' in order else order['id'], 'side': order['side'], - 'price': Decimal(order['price']), - 'size': Decimal(order.get('size', order['remaining_size'])) + 'price': float(order['price']), + 'size': float(order['size']) if 'size' in order else float(order['remaining_size']) } if order['side'] == 'buy': bids = self.get_bids(order['price']) @@ -103,7 +104,7 @@ def add(self, order): self.set_asks(order['price'], asks) def remove(self, order): - price = Decimal(order['price']) + price = float(order['price']) if order['side'] == 'buy': bids = self.get_bids(price) if bids is not None: @@ -122,8 +123,8 @@ def remove(self, order): self.remove_asks(price) def match(self, order): - size = Decimal(order['size']) - price = Decimal(order['price']) + size = float(order['size']) + price = float(order['price']) if order['side'] == 'buy': bids = self.get_bids(price) @@ -147,8 +148,8 @@ def match(self, order): self.set_asks(price, asks) def change(self, order): - new_size = Decimal(order['new_size']) - price = Decimal(order['price']) + new_size = float(order['new_size']) + price = float(order['price']) if order['side'] == 'buy': bids = self.get_bids(price) @@ -205,12 +206,6 @@ def get_current_book(self): ]) return result - def get_deque(self): - return self._deque - - def clear_deque(self): - self._deque.clear() - def get_ask(self): return self._asks.min_key() From 19a2485f149942d1da59aa9412fd35e654180b84 Mon Sep 17 00:00:00 2001 From: Ted McCormack Date: Tue, 30 May 2017 20:55:49 -0700 Subject: [PATCH 017/174] Adding logging to OrderBook --- GDAX/OrderBook.py | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/GDAX/OrderBook.py b/GDAX/OrderBook.py index 1a0f75c5..c36c70cb 100644 --- a/GDAX/OrderBook.py +++ b/GDAX/OrderBook.py @@ -6,6 +6,7 @@ from operator import itemgetter from bintrees import RBTree +from decimal import Decimal import pickle from GDAX.PublicClient import PublicClient @@ -37,22 +38,23 @@ def onMessage(self, message): self.add({ 'id': bid[2], 'side': 'buy', - 'price': float(bid[0]), - 'size': float(bid[1]) + 'price': Decimal(bid[0]), + 'size': Decimal(bid[1]) }) for ask in res['asks']: self.add({ 'id': ask[2], 'side': 'sell', - 'price': float(ask[0]), - 'size': float(ask[1]) + 'price': Decimal(ask[0]), + 'size': Decimal(ask[1]) }) self._sequence = res['sequence'] if sequence <= self._sequence: return #ignore old messages elif sequence > self._sequence + 1: - self.reset() + self.close() + self.start() return # print(message) @@ -75,18 +77,13 @@ def onMessage(self, message): # asks = self.get_asks(ask) # ask_depth = sum([a['size'] for a in asks]) # print('bid: %f @ %f - ask: %f @ %f' % (bid_depth, bid, ask_depth, ask)) - def reset(self): - print("Restarting thread.") - self.close() - self.start() - self._sequence = -1 def add(self, order): order = { 'id': order['order_id'] if 'order_id' in order else order['id'], 'side': order['side'], - 'price': float(order['price']), - 'size': float(order['size']) if 'size' in order else float(order['remaining_size']) + 'price': Decimal(order['price']), + 'size': Decimal(order.get('size', order['remaining_size'])) } if order['side'] == 'buy': bids = self.get_bids(order['price']) @@ -104,7 +101,7 @@ def add(self, order): self.set_asks(order['price'], asks) def remove(self, order): - price = float(order['price']) + price = Decimal(order['price']) if order['side'] == 'buy': bids = self.get_bids(price) if bids is not None: @@ -123,8 +120,8 @@ def remove(self, order): self.remove_asks(price) def match(self, order): - size = float(order['size']) - price = float(order['price']) + size = Decimal(order['size']) + price = Decimal(order['price']) if order['side'] == 'buy': bids = self.get_bids(price) @@ -148,8 +145,8 @@ def match(self, order): self.set_asks(price, asks) def change(self, order): - new_size = float(order['new_size']) - price = float(order['price']) + new_size = Decimal(order['new_size']) + price = Decimal(order['price']) if order['side'] == 'buy': bids = self.get_bids(price) From 9c365de78baf7468df64b89d98b498542a2f4d01 Mon Sep 17 00:00:00 2001 From: Ted McCormack Date: Tue, 30 May 2017 20:56:43 -0700 Subject: [PATCH 018/174] Theirs --- GDAX/WebsocketClient.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/GDAX/WebsocketClient.py b/GDAX/WebsocketClient.py index 0a01bacf..da3c0190 100644 --- a/GDAX/WebsocketClient.py +++ b/GDAX/WebsocketClient.py @@ -11,14 +11,13 @@ from websocket import create_connection class WebsocketClient(object): - # Need to make a server to test. def __init__(self, url=None, products=None, type=None): if url is None: url = "wss://ws-feed.gdax.com" self.url = url self.products = products - self.type = type or "subscribe" + self.type = "subscribe" #type or "subscribe" self.stop = False self.ws = None self.thread = None @@ -29,6 +28,7 @@ def _go(): self._listen() self.onOpen() + self.ws = create_connection(self.url) self.thread = Thread(target=_go) self.thread.start() @@ -42,7 +42,6 @@ def _connect(self): self.url = self.url[:-1] self.stop = False - self.ws = create_connection(self.url) sub_params = {'type': 'subscribe', 'product_ids': self.products} self.ws.send(json.dumps(sub_params)) if self.type == "heartbeat": From 255140d7bc834e3a889f453ead1f913bbc06a7cd Mon Sep 17 00:00:00 2001 From: Paul Mestemaker Date: Wed, 31 May 2017 20:13:48 -0700 Subject: [PATCH 019/174] Hotfix: KeyError on 'remaining_size' This will always attempt to find 'remaining_size' in order even if 'size' exists... ``` 'size': Decimal(order.get('size', order['remaining_size'])) ``` This will only attempt to find 'remaining_size' if order.get('size') returns a Falsey value ``` 'size': Decimal(order.get('size') or order['remaining_size']) ``` Misc improvements: * Added comments * Added logging * Minor formatting --- GDAX/OrderBook.py | 19 +++++++++++-------- GDAX/WebsocketClient.py | 6 +++--- contributors.txt | 1 + 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/GDAX/OrderBook.py b/GDAX/OrderBook.py index e6a15957..511ab49f 100644 --- a/GDAX/OrderBook.py +++ b/GDAX/OrderBook.py @@ -13,7 +13,6 @@ class OrderBook(WebsocketClient): - def __init__(self, product_id='BTC-USD'): WebsocketClient.__init__(self, products=product_id) self._asks = RBTree() @@ -44,8 +43,10 @@ def onMessage(self, message): self._sequence = res['sequence'] if sequence <= self._sequence: - return #ignore old messages + # ignore older messages (e.g. before order book initialization from getProductOrderBook) + return elif sequence > self._sequence + 1: + print('Error: messages missing ({} - {}). Re-initializing websocket.'.format(sequence, self._sequence)) self.close() self.start() return @@ -73,10 +74,10 @@ def onMessage(self, message): def add(self, order): order = { - 'id': order['order_id'] if 'order_id' in order else order['id'], + 'id': order.get('order_id') or order['id'], 'side': order['side'], 'price': Decimal(order['price']), - 'size': Decimal(order.get('size', order['remaining_size'])) + 'size': Decimal(order.get('size') or order['remaining_size']) } if order['side'] == 'buy': bids = self.get_bids(order['price']) @@ -163,10 +164,11 @@ def change(self, order): return def get_current_book(self): - result = dict() - result['sequence'] = self._sequence - result['asks'] = list() - result['bids'] = list() + result = { + 'sequence': self._sequence, + 'asks': [], + 'bids': [], + } for ask in self._asks: try: # There can be a race condition here, where a price point is removed @@ -223,6 +225,7 @@ def set_bids(self, price, bids): if __name__ == '__main__': import time + order_book = OrderBook() order_book.start() time.sleep(10) diff --git a/GDAX/WebsocketClient.py b/GDAX/WebsocketClient.py index da3c0190..f9fb097c 100644 --- a/GDAX/WebsocketClient.py +++ b/GDAX/WebsocketClient.py @@ -88,11 +88,11 @@ def onOpen(self): self.url = "wss://ws-feed.gdax.com/" self.products = ["BTC-USD", "ETH-USD"] self.MessageCount = 0 - print ("Lets count the messages!") + print("Let's count the messages!") def onMessage(self, msg): if 'price' in msg and 'type' in msg: - print ("Message type:", msg["type"], "\t@ %.3f" % float(msg["price"])) + print("Message type:", msg["type"], "\t@ %.3f" % float(msg["price"])) self.MessageCount += 1 def onClose(self): @@ -103,6 +103,6 @@ def onClose(self): print(wsClient.url, wsClient.products) # Do some logic with the data while (wsClient.MessageCount < 500): - print ("\nMessageCount =", "%i \n" % wsClient.MessageCount) + print("\nMessageCount =", "%i \n" % wsClient.MessageCount) time.sleep(1) wsClient.close() diff --git a/contributors.txt b/contributors.txt index 66b5190d..a2b3a3fe 100644 --- a/contributors.txt +++ b/contributors.txt @@ -2,3 +2,4 @@ Daniel J Paquin Leonard Lin Jeff Gibson David Caseria +Paul Mestemaker From f3c2dd86a313fac4949857478a42b0547ed71886 Mon Sep 17 00:00:00 2001 From: Ted McCormack Date: Wed, 31 May 2017 21:39:05 -0700 Subject: [PATCH 020/174] Adding Ticker websocket client, modified PublicClient to only return urls if url_only --- GDAX/OrderBook.py | 15 ++++++------ GDAX/PublicClient.py | 53 ++++++++++++++++++++++++++++++++++------- GDAX/Ticker.py | 42 ++++++++++++++++++++++++++++++++ GDAX/WebsocketClient.py | 2 +- 4 files changed, 94 insertions(+), 18 deletions(-) create mode 100644 GDAX/Ticker.py diff --git a/GDAX/OrderBook.py b/GDAX/OrderBook.py index c36c70cb..66e2c330 100644 --- a/GDAX/OrderBook.py +++ b/GDAX/OrderBook.py @@ -15,19 +15,19 @@ class OrderBook(WebsocketClient): - def __init__(self, product_id='BTC-USD', logTo=None): + def __init__(self, product_id='BTC-USD', log_to=None): WebsocketClient.__init__(self, products=product_id) self._asks = RBTree() self._bids = RBTree() self._client = PublicClient(product_id=product_id) self._sequence = -1 - if logTo: - assert hasattr(logTo, 'write') - self._logTo = logTo + if log_to: + assert hasattr(log_to, 'write') + self._log_to = log_to def onMessage(self, message): - if self._logTo: - pickle.dump(message, self._logTo) + if self._log_to: + pickle.dump(message, self._log_to) sequence = message['sequence'] if self._sequence == -1: @@ -57,7 +57,6 @@ def onMessage(self, message): self.start() return - # print(message) msg_type = message['type'] if msg_type == 'open': self.add(message) @@ -83,7 +82,7 @@ def add(self, order): 'id': order['order_id'] if 'order_id' in order else order['id'], 'side': order['side'], 'price': Decimal(order['price']), - 'size': Decimal(order.get('size', order['remaining_size'])) + 'size': Decimal(order['size'] if 'size' in order else order['remaining_size']) } if order['side'] == 'buy': bids = self.get_bids(order['price']) diff --git a/GDAX/PublicClient.py b/GDAX/PublicClient.py index fda7f8a9..5b11c6b8 100644 --- a/GDAX/PublicClient.py +++ b/GDAX/PublicClient.py @@ -7,14 +7,23 @@ import requests class PublicClient(): - def __init__(self, api_url="https://api.gdax.com", product_id="BTC-USD"): + def __init__( + self, + api_url="https://api.gdax.com", + product_id="BTC-USD", + url_only=False + ): self.url = api_url + self.url_only = url_only if api_url[-1] == "/": self.url = api_url[:-1] self.productId = product_id def getProducts(self): - r = requests.get(self.url + '/products') + url = self.url + '/products' + + if self.url_only: return url + r = requests.get(url) #r.raise_for_status() return r.json() @@ -22,21 +31,33 @@ def getProductOrderBook(self, json=None, level=2, product=''): if type(json) is dict: if "product" in json: product = json["product"] if "level" in json: level = json['level'] - r = requests.get(self.url + '/products/%s/book?level=%s' % (product or self.productId, str(level))) + + url = self.url + '/products/%s/book?level=%s' % (product or self.productId, str(level)) + + if self.url_only: return url + r = requests.get(url) #r.raise_for_status() return r.json() def getProductTicker(self, json=None, product=''): if type(json) is dict: if "product" in json: product = json["product"] - r = requests.get(self.url + '/products/%s/ticker' % (product or self.productId)) + + url = self.url + '/products/%s/ticker' % (product or self.productId) + + if self.url_only: return url + r = requests.get(url) #r.raise_for_status() return r.json() def getProductTrades(self, json=None, product=''): if type(json) is dict: if "product" in json: product = json["product"] - r = requests.get(self.url + '/products/%s/trades' % (product or self.productId)) + + url = self.url + '/products/%s/trades' % (product or self.productId) + + if self.url_only: return url + r = requests.get(url) #r.raise_for_status() return r.json() @@ -49,23 +70,37 @@ def getProductHistoricRates(self, json=None, product='', start='', end='', granu payload["start"] = start payload["end"] = end payload["granularity"] = granularity - r = requests.get(self.url + '/products/%s/candles' % (product or self.productId), params=payload) + + url = self.url + '/products/%s/candles' % (product or self.productId) + + if self.url_only: return url + r = requests.get(url, params=payload) #r.raise_for_status() return r.json() def getProduct24HrStats(self, json=None, product=''): if type(json) is dict: if "product" in json: product = json["product"] - r = requests.get(self.url + '/products/%s/stats' % (product or self.productId)) + + url = self.url + '/products/%s/stats' % (product or self.productId) + + if self.url_only: return url + r = requests.get(url) #r.raise_for_status() return r.json() def getCurrencies(self): - r = requests.get(self.url + '/currencies') + url = self.url + '/currencies' + + if self.url_only: return url + r = requests.get(url) #r.raise_for_status() return r.json() def getTime(self): - r = requests.get(self.url + '/time') + url = self.url + '/time' + + if self.url_only: return url + r = requests.get(url) #r.raise_for_status() return r.json() diff --git a/GDAX/Ticker.py b/GDAX/Ticker.py new file mode 100644 index 00000000..cbd7194a --- /dev/null +++ b/GDAX/Ticker.py @@ -0,0 +1,42 @@ +# +# GDAX/OrderBook.py +# David Caseria +# +# Live order book updated from the GDAX Websocket Feed + +from GDAX.PublicClient import PublicClient +from GDAX.WebsocketClient import WebsocketClient + +class Ticker(WebsocketClient): + + def __init__(self, product_id='BTC-USD', log_to=None): + ticker_url = PublicClient( + api_url='wss://ws-feed.gdax.com', + product_id=product_id, + url_only=True + ).getProductTicker() + + WebsocketClient.__init__(self, url=ticker_url, products=product_id) + + if log_to: + assert hasattr(log_to, 'write') + self._log_to = log_to + self._current_ticker = None + + def onMessage(self, message): + if self._log_to: + pickle.dump(message, self._log_to) + + self._current_ticker = message + + def get_current_ticker(self): + return self._current_ticker + +if __name__ == '__main__': + import time + ticker = Ticker() + ticker.start() + while True: + print(ticker.get_current_ticker()) + time.sleep(10) + ticker.close() diff --git a/GDAX/WebsocketClient.py b/GDAX/WebsocketClient.py index da3c0190..94596ec0 100644 --- a/GDAX/WebsocketClient.py +++ b/GDAX/WebsocketClient.py @@ -17,7 +17,7 @@ def __init__(self, url=None, products=None, type=None): self.url = url self.products = products - self.type = "subscribe" #type or "subscribe" + self.type = type or "subscribe" self.stop = False self.ws = None self.thread = None From 296d5d4f9a5b6091750916465ad76b583c2129f5 Mon Sep 17 00:00:00 2001 From: Ted McCormack Date: Wed, 31 May 2017 21:50:31 -0700 Subject: [PATCH 021/174] Forgot to include Ticker in __init__ --- GDAX/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/GDAX/__init__.py b/GDAX/__init__.py index 03d6b8a9..d00382bd 100644 --- a/GDAX/__init__.py +++ b/GDAX/__init__.py @@ -2,3 +2,4 @@ from GDAX.PublicClient import PublicClient from GDAX.WebsocketClient import WebsocketClient from GDAX.OrderBook import OrderBook +from GDAX.Ticker import Ticker From 6d8df5ce420b95c24829bf2258a3c5baae115225 Mon Sep 17 00:00:00 2001 From: Bryan Kaplan <#@bryankaplan.com> Date: Wed, 31 May 2017 22:25:41 -0700 Subject: [PATCH 022/174] Update documentation to match #42. --- README.md | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 76bcb4bc..1ddbc496 100644 --- a/README.md +++ b/README.md @@ -154,21 +154,15 @@ authClient.getAccountHolds("7d0f7d8e-dd34-4d9c-a846-06f431c381ba") - [buy & sell](https://docs.gdax.com/#place-a-new-order) ```python # Buy 0.01 BTC @ 100 USD -buyParams = { - 'price': '100.00', #USD - 'size': '0.01', #BTC - 'product_id': 'BTC-USD' -} -authClient.buy(buyParams) +authClient.buy(price='100.00', #USD + size='0.01', #BTC + product_id='BTC-USD') ``` ```python # Sell 0.01 BTC @ 200 USD -sellParams = { - 'price': '200.00', #USD - 'size': '0.01', #BTC - #product_id not needed if default is desired -} -authClient.sell(sellParams) +authClient.sell(price='200.00', #USD + size='0.01', #BTC + product_id='BTC-USD') ``` - [cancelOrder](https://docs.gdax.com/#cancel-an-order) From d6120c10e95aeaab951a2b448e69e5a80799ebc9 Mon Sep 17 00:00:00 2001 From: Bryan Kaplan <#@bryankaplan.com> Date: Wed, 31 May 2017 22:30:57 -0700 Subject: [PATCH 023/174] Fix docs regarding AuthenticatedClient inheritance Previously it said that `AuthenticatedClient` inherits all methods from the `PrivateClient`. But there is no `PrivateClient`. Really `AuthenticatedClient` inherits from `PublicClient`. I also took the opportunity to improve this paragraph's markup formatting. It's good to follow the PEP-8 convention that states: > For flowing long blocks of text with fewer structural restrictions > (docstrings or comments), the line length should be limited to 72 > characters. Additionally I reduced the backticks from three to one for inline code. Triple backticks should not be necessary for GitHub markdown. --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ddbc496..510d85bd 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,14 @@ print (method1, method2) ### Authenticated Client -Not all API endpoints are available to everyone. Those requiring user authentication can be reached using ```AuthenticatedClient```. You must setup API access within your [account settings](https://www.gdax.com/settings/api). The ```AuthenticatedClient``` inherits all methods from the ```PrivateClient``` class, so you will only need to initialize one if you are planning to integrate both into your script. + +Not all API endpoints are available to everyone. +Those requiring user authentication can be reached using `AuthenticatedClient`. +You must setup API access within your +[account settings](https://www.gdax.com/settings/api). +The `AuthenticatedClient` inherits all methods from the `PublicClient` +class, so you will only need to initialize one if you are planning to +integrate both into your script. ```python import GDAX From 28e5b5071e725baea8237c7f9b7168a5ba68bd0f Mon Sep 17 00:00:00 2001 From: Ted McCormack Date: Thu, 1 Jun 2017 09:13:04 -0700 Subject: [PATCH 024/174] Only take match messages as per API doc, no volume info for now --- GDAX/Ticker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/GDAX/Ticker.py b/GDAX/Ticker.py index cbd7194a..442e37b8 100644 --- a/GDAX/Ticker.py +++ b/GDAX/Ticker.py @@ -27,7 +27,8 @@ def onMessage(self, message): if self._log_to: pickle.dump(message, self._log_to) - self._current_ticker = message + if 'type' in message and message['type'] == 'match': + self._current_ticker = message def get_current_ticker(self): return self._current_ticker From 0af8bd450b6d9acb538dccc650bf46491f4b46cb Mon Sep 17 00:00:00 2001 From: Ted McCormack Date: Thu, 1 Jun 2017 09:14:37 -0700 Subject: [PATCH 025/174] Only log match --- GDAX/Ticker.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/GDAX/Ticker.py b/GDAX/Ticker.py index 442e37b8..c3890929 100644 --- a/GDAX/Ticker.py +++ b/GDAX/Ticker.py @@ -24,11 +24,10 @@ def __init__(self, product_id='BTC-USD', log_to=None): self._current_ticker = None def onMessage(self, message): - if self._log_to: - pickle.dump(message, self._log_to) - if 'type' in message and message['type'] == 'match': self._current_ticker = message + if self._log_to: + pickle.dump(message, self._log_to) def get_current_ticker(self): return self._current_ticker From b95b0aaa4a3e382469f406997cd2b3305a06a6f8 Mon Sep 17 00:00:00 2001 From: Sebastian Wozny Date: Fri, 2 Jun 2017 11:14:06 +0200 Subject: [PATCH 026/174] Update README.md Fix code sample --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 76bcb4bc..64ec440a 100644 --- a/README.md +++ b/README.md @@ -96,12 +96,12 @@ Only available for the `PublicClient`, you may pass any function above raw JSON import GDAX publicClient = GDAX.PublicClient() -method1 = public.getProductHistoricRates(granularity='3000') +method1 = publicClient.getProductHistoricRates(granularity='3000') params = { 'granularity': '3000' } -method2 = public.getProductHistoricRates(params) +method2 = publicClient.getProductHistoricRates(params) # Both methods will send the same request, but not always return the same data if run in series. print (method1, method2) From 151d016b01531a55568addd625add022e36705fd Mon Sep 17 00:00:00 2001 From: Ken Fehling Date: Fri, 9 Jun 2017 13:19:59 -0400 Subject: [PATCH 027/174] Fixed docs for cancelAll --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e42758e5..87203599 100644 --- a/README.md +++ b/README.md @@ -176,9 +176,9 @@ authClient.sell(price='200.00', #USD ```python authClient.cancelOrder("d50ec984-77a8-460a-b958-66f114b0de9b") ``` -- [cancelAll](https://docs.gdax.com/#cancel-an-order) +- [cancelAll](https://docs.gdax.com/#cancel-all) ```python -authClient.cancelOrder(productId='BTC-USD') +authClient.cancelAll(product='BTC-USD') ``` - [getOrders](https://docs.gdax.com/#list-orders) (paginated) From 68358037db4ab280e4fb671ebf52688a0d1821a8 Mon Sep 17 00:00:00 2001 From: acontry Date: Sat, 10 Jun 2017 16:58:45 -0700 Subject: [PATCH 028/174] Rename package and files to lowercase. --- {GDAX => gdax}/.gitignore | 0 {GDAX => gdax}/__init__.py | 0 GDAX/AuthenticatedClient.py => gdax/authenticated_client.py | 0 GDAX/OrderBook.py => gdax/order_book.py | 0 GDAX/PublicClient.py => gdax/public_client.py | 0 GDAX/WebsocketClient.py => gdax/websocket_client.py | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename {GDAX => gdax}/.gitignore (100%) rename {GDAX => gdax}/__init__.py (100%) rename GDAX/AuthenticatedClient.py => gdax/authenticated_client.py (100%) rename GDAX/OrderBook.py => gdax/order_book.py (100%) rename GDAX/PublicClient.py => gdax/public_client.py (100%) rename GDAX/WebsocketClient.py => gdax/websocket_client.py (100%) diff --git a/GDAX/.gitignore b/gdax/.gitignore similarity index 100% rename from GDAX/.gitignore rename to gdax/.gitignore diff --git a/GDAX/__init__.py b/gdax/__init__.py similarity index 100% rename from GDAX/__init__.py rename to gdax/__init__.py diff --git a/GDAX/AuthenticatedClient.py b/gdax/authenticated_client.py similarity index 100% rename from GDAX/AuthenticatedClient.py rename to gdax/authenticated_client.py diff --git a/GDAX/OrderBook.py b/gdax/order_book.py similarity index 100% rename from GDAX/OrderBook.py rename to gdax/order_book.py diff --git a/GDAX/PublicClient.py b/gdax/public_client.py similarity index 100% rename from GDAX/PublicClient.py rename to gdax/public_client.py diff --git a/GDAX/WebsocketClient.py b/gdax/websocket_client.py similarity index 100% rename from GDAX/WebsocketClient.py rename to gdax/websocket_client.py From cedc00e419a4d6c8455310312175971a447a7481 Mon Sep 17 00:00:00 2001 From: acontry Date: Sat, 10 Jun 2017 17:55:40 -0700 Subject: [PATCH 029/174] PEP 8 formatting. --- gdax/__init__.py | 8 +- gdax/authenticated_client.py | 242 +++++++++++++++++++---------------- gdax/order_book.py | 28 ++-- gdax/public_client.py | 65 +++++----- gdax/websocket_client.py | 47 +++---- setup.py | 20 +-- 6 files changed, 215 insertions(+), 195 deletions(-) diff --git a/gdax/__init__.py b/gdax/__init__.py index 03d6b8a9..8f0b656d 100644 --- a/gdax/__init__.py +++ b/gdax/__init__.py @@ -1,4 +1,4 @@ -from GDAX.AuthenticatedClient import AuthenticatedClient -from GDAX.PublicClient import PublicClient -from GDAX.WebsocketClient import WebsocketClient -from GDAX.OrderBook import OrderBook +from gdax.AuthenticatedClient import AuthenticatedClient +from gdax.PublicClient import PublicClient +from gdax.WebsocketClient import WebsocketClient +from gdax.OrderBook import OrderBook diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index 360f049e..7c56d9ab 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -4,66 +4,72 @@ # # For authenticated requests to the GDAX exchange -import hmac, hashlib, time, requests, base64, json +import hmac +import hashlib +import time +import requests +import base64 +import json from requests.auth import AuthBase -from GDAX.PublicClient import PublicClient +from gdax.PublicClient import PublicClient + class AuthenticatedClient(PublicClient): def __init__(self, key, b64secret, passphrase, api_url="https://api.gdax.com", product_id="BTC-USD"): self.url = api_url if api_url[-1] == "/": self.url = api_url[:-1] - self.productId = product_id + self.product_id = product_id self.auth = GdaxAuth(key, b64secret, passphrase) - def getAccount(self, accountId): - r = requests.get(self.url + '/accounts/' + accountId, auth=self.auth) - #r.raise_for_status() + def get_account(self, account_id): + r = requests.get(self.url + '/accounts/' + account_id, auth=self.auth) + # r.raise_for_status() return r.json() - def getAccounts(self): - return self.getAccount('') + def get_accounts(self): + return self.get_account('') - def getAccountHistory(self, accountId): - list = [] - r = requests.get(self.url + '/accounts/%s/ledger' %accountId, auth=self.auth) - #r.raise_for_status() - list.append(r.json()) + def get_account_history(self, account_id): + result = [] + r = requests.get(self.url + '/accounts/%s/ledger' % account_id, auth=self.auth) + # r.raise_for_status() + result.append(r.json()) if "cb-after" in r.headers: - self.historyPagination(accountId, list, r.headers["cb-after"]) - return list + self.history_pagination(account_id, result, r.headers["cb-after"]) + return result - def historyPagination(self, accountId, list, after): - r = requests.get(self.url + '/accounts/%s/ledger?after=%s' %(accountId, str(after)), auth=self.auth) - #r.raise_for_status() + def history_pagination(self, account_id, result, after): + r = requests.get(self.url + '/accounts/%s/ledger?after=%s' % (account_id, str(after)), auth=self.auth) + # r.raise_for_status() if r.json(): - list.append(r.json()) + result.append(r.json()) if "cb-after" in r.headers: - self.historyPagination(accountId, list, r.headers["cb-after"]) - return list - - def getAccountHolds(self, accountId): - list = [] - r = requests.get(self.url + '/accounts/%s/holds' %accountId, auth=self.auth) - #r.raise_for_status() - list.append(r.json()) + self.history_pagination(account_id, result, r.headers["cb-after"]) + return result + + def get_account_holds(self, account_id): + result = [] + r = requests.get(self.url + '/accounts/%s/holds' % account_id, auth=self.auth) + # r.raise_for_status() + result.append(r.json()) if "cb-after" in r.headers: - self.holdsPagination(accountId, list, r.headers["cb-after"]) - return list + self.holds_pagination(account_id, result, r.headers["cb-after"]) + return result - def holdsPagination(self, accountId, list, after): - r = requests.get(self.url + '/accounts/%s/holds?after=%s' %(accountId, str(after)), auth=self.auth) - #r.raise_for_status() + def holds_pagination(self, account_id, result, after): + r = requests.get(self.url + '/accounts/%s/holds?after=%s' % (account_id, str(after)), auth=self.auth) + # r.raise_for_status() if r.json(): - list.append(r.json()) + result.append(r.json()) if "cb-after" in r.headers: - self.holdsPagination(accountId, list, r.headers["cb-after"]) - return list + self.holds_pagination(account_id, result, r.headers["cb-after"]) + return result def buy(self, **kwargs): kwargs["side"] = "buy" - if not "product_id" in kwargs: - kwargs["product_id"] = self.productId + if "product_id" not in kwargs: + kwargs["product_id"] = self.product_id r = requests.post(self.url + '/orders', data=json.dumps(kwargs), auth=self.auth) @@ -76,106 +82,118 @@ def sell(self, **kwargs): auth=self.auth) return r.json() - def cancelOrder(self, orderId): - r = requests.delete(self.url + '/orders/' + orderId, auth=self.auth) - #r.raise_for_status() + def cancel_order(self, order_id): + r = requests.delete(self.url + '/orders/' + order_id, auth=self.auth) + # r.raise_for_status() return r.json() - def cancelAll(self, data=None, product=''): + def cancel_all(self, data=None, product=''): if type(data) is dict: - if "product" in data: product = data["product"] - r = requests.delete(self.url + '/orders/', data=json.dumps({'product_id':product or self.productId}), auth=self.auth) - #r.raise_for_status() + if "product" in data: + product = data["product"] + r = requests.delete(self.url + '/orders/', + data=json.dumps({'product_id': product or self.product_id}), auth=self.auth) + # r.raise_for_status() return r.json() - def getOrder(self, orderId): - r = requests.get(self.url + '/orders/' + orderId, auth=self.auth) - #r.raise_for_status() + def get_order(self, order_id): + r = requests.get(self.url + '/orders/' + order_id, auth=self.auth) + # r.raise_for_status() return r.json() - def getOrders(self): - list = [] + def get_orders(self): + result = [] r = requests.get(self.url + '/orders/', auth=self.auth) - #r.raise_for_status() - list.append(r.json()) + # r.raise_for_status() + result.append(r.json()) if 'cb-after' in r.headers: - self.paginateOrders(list, r.headers['cb-after']) - return list + self.paginate_orders(result, r.headers['cb-after']) + return result - def paginateOrders(self, list, after): - r = requests.get(self.url + '/orders?after=%s' %str(after)) - #r.raise_for_status() + def paginate_orders(self, result, after): + r = requests.get(self.url + '/orders?after=%s' % str(after)) + # r.raise_for_status() if r.json(): - list.append(r.json()) + result.append(r.json()) if 'cb-after' in r.headers: - self.paginateOrders(list, r.headers['cb-after']) - return list + self.paginate_orders(result, r.headers['cb-after']) + return result - def getFills(self, orderId='', productId='', before='', after='', limit=''): - list = [] + def get_fills(self, order_id='', product_id='', before='', after='', limit=''): + result = [] url = self.url + '/fills?' - if orderId: url += "order_id=%s&" %str(orderId) - if productId: url += "product_id=%s&" %(productId or self.productId) - if before: url += "before=%s&" %str(before) - if after: url += "after=%s&" %str(after) - if limit: url += "limit=%s&" %str(limit) + if order_id: + url += "order_id=%s&" % str(order_id) + if product_id: + url += "product_id=%s&" % (product_id or self.product_id) + if before: + url += "before=%s&" % str(before) + if after: + url += "after=%s&" % str(after) + if limit: + url += "limit=%s&" % str(limit) r = requests.get(url, auth=self.auth) - #r.raise_for_status() - list.append(r.json()) + # r.raise_for_status() + result.append(r.json()) if 'cb-after' in r.headers and limit is not len(r.json()): - return self.paginateFills(list, r.headers['cb-after'], orderId=orderId, productId=productId) - return list + return self.paginate_fills(result, r.headers['cb-after'], order_id=order_id, product_id=product_id) + return result - def paginateFills(self, list, after, orderId='', productId=''): + def paginate_fills(self, result, after, order_id='', product_id=''): url = self.url + '/fills?after=%s&' % str(after) - if orderId: url += "order_id=%s&" % str(orderId) - if productId: url += "product_id=%s&" % (productId or self.productId) + if order_id: + url += "order_id=%s&" % str(order_id) + if product_id: + url += "product_id=%s&" % (product_id or self.product_id) r = requests.get(url, auth=self.auth) - #r.raise_for_status() + # r.raise_for_status() if r.json(): - list.append(r.json()) + result.append(r.json()) if 'cb-after' in r.headers: - return self.paginateFills(list, r.headers['cb-after'], orderId=orderId, productId=productId) - return list + return self.paginate_fills(result, r.headers['cb-after'], order_id=order_id, product_id=product_id) + return result - def getFundings(self, list='', status='', after=''): - if not list: list = [] + def get_fundings(self, result='', status='', after=''): + if not result: + result = [] url = self.url + '/funding?' - if status: url += "status=%s&" % str(status) - if after: url += 'after=%s&' % str(after) + if status: + url += "status=%s&" % str(status) + if after: + url += 'after=%s&' % str(after) r = requests.get(url, auth=self.auth) - #r.raise_for_status() - list.append(r.json()) + # r.raise_for_status() + result.append(r.json()) if 'cb-after' in r.headers: - return self.getFundings(list, status=status, after=r.headers['cb-after']) - return list + return self.get_fundings(result, status=status, after=r.headers['cb-after']) + return result - def repayFunding(self, amount='', currency=''): + def repay_funding(self, amount='', currency=''): payload = { "amount": amount, - "currency": currency #example: USD + "currency": currency # example: USD } r = requests.post(self.url + "/funding/repay", data=json.dumps(payload), auth=self.auth) - #r.raise_for_status() + # r.raise_for_status() return r.json() - def marginTransfer(self, margin_profile_id="", type="",currency="",amount=""): + def margin_transfer(self, margin_profile_id="", transfer_type="", currency="", amount=""): payload = { "margin_profile_id": margin_profile_id, - "type": type, - "currency": currency, # example: USD + "type": transfer_type, + "currency": currency, # example: USD "amount": amount } r = requests.post(self.url + "/profiles/margin-transfer", data=json.dumps(payload), auth=self.auth) # r.raise_for_status() return r.json() - def getPosition(self): + def get_position(self): r = requests.get(self.url + "/position", auth=self.auth) # r.raise_for_status() return r.json() - def closePosition(self, repay_only=""): + def close_position(self, repay_only=""): payload = { "repay_only": repay_only or False } @@ -190,10 +208,10 @@ def deposit(self, amount="", currency="", payment_method_id=""): "payment_method_id": payment_method_id } r = requests.post(self.url + "/deposits/payment-method", data=json.dumps(payload), auth=self.auth) - #r.raise_for_status() + # r.raise_for_status() return r.json() - def coinbaseDeposit(self, amount="", currency="", coinbase_account_id=""): + def coinbase_deposit(self, amount="", currency="", coinbase_account_id=""): payload = { "amount": amount, "currency": currency, @@ -210,10 +228,10 @@ def withdraw(self, amount="", currency="", payment_method_id=""): "payment_method_id": payment_method_id } r = requests.post(self.url + "/withdrawals/payment-method", data=json.dumps(payload), auth=self.auth) - #r.raise_for_status() + # r.raise_for_status() return r.json() - def coinbaseWithdraw(self, amount="", currency="", coinbase_account_id=""): + def coinbase_withdraw(self, amount="", currency="", coinbase_account_id=""): payload = { "amount": amount, "currency": currency, @@ -223,7 +241,7 @@ def coinbaseWithdraw(self, amount="", currency="", coinbase_account_id=""): # r.raise_for_status() return r.json() - def cryptoWithdraw(self, amount="", currency="", crypto_address=""): + def crypto_withdraw(self, amount="", currency="", crypto_address=""): payload = { "amount": amount, "currency": currency, @@ -233,40 +251,42 @@ def cryptoWithdraw(self, amount="", currency="", crypto_address=""): # r.raise_for_status() return r.json() - def getPaymentMethods(self): + def get_payment_methods(self): r = requests.get(self.url + "/payment-methods", auth=self.auth) - #r.raise_for_status() + # r.raise_for_status() return r.json() - def getCoinbaseAccounts(self): + def get_coinbase_accounts(self): r = requests.get(self.url + "/coinbase-accounts", auth=self.auth) - #r.raise_for_status() + # r.raise_for_status() return r.json() - def createReport(self, type="", start_date="", end_date="", product_id="", account_id="", format="", email=""): + def create_report(self, report_type="", start_date="", end_date="", product_id="", account_id="", report_format="", + email=""): payload = { - "type": type, + "type": report_type, "start_date": start_date, "end_date": end_date, "product_id": product_id, "account_id": account_id, - "format": format, + "format": report_format, "email": email } r = requests.post(self.url + "/reports", data=json.dumps(payload), auth=self.auth) - #r.raise_for_status() + # r.raise_for_status() return r.json() - def getReport(self, reportId=""): - r = requests.get(self.url + "/reports/" + reportId, auth=self.auth) - #r.raise_for_status() + def get_report(self, report_id=""): + r = requests.get(self.url + "/reports/" + report_id, auth=self.auth) + # r.raise_for_status() return r.json() - def getTrailingVolume(self): + def get_trailing_volume(self): r = requests.get(self.url + "/users/self/trailing-volume", auth=self.auth) - #r.raise_for_status() + # r.raise_for_status() return r.json() + class GdaxAuth(AuthBase): # Provided by GDAX: https://docs.gdax.com/#signing-a-message def __init__(self, api_key, secret_key, passphrase): diff --git a/gdax/order_book.py b/gdax/order_book.py index 511ab49f..012aa7dc 100644 --- a/gdax/order_book.py +++ b/gdax/order_book.py @@ -8,8 +8,8 @@ from bintrees import RBTree from decimal import Decimal -from GDAX.PublicClient import PublicClient -from GDAX.WebsocketClient import WebsocketClient +from gdax.PublicClient import PublicClient +from gdax.WebsocketClient import WebsocketClient class OrderBook(WebsocketClient): @@ -20,12 +20,12 @@ def __init__(self, product_id='BTC-USD'): self._client = PublicClient(product_id=product_id) self._sequence = -1 - def onMessage(self, message): + def on_message(self, message): sequence = message['sequence'] if self._sequence == -1: self._asks = RBTree() self._bids = RBTree() - res = self._client.getProductOrderBook(level=3) + res = self._client.get_product_order_book(level=3) for bid in res['bids']: self.add({ 'id': bid[2], @@ -173,29 +173,21 @@ def get_current_book(self): try: # There can be a race condition here, where a price point is removed # between these two ops - thisAsk = self._asks[ask] + this_ask = self._asks[ask] except KeyError: continue - for order in thisAsk: - result['asks'].append([ - order['price'], - order['size'], - order['id'], - ]) + for order in this_ask: + result['asks'].append([order['price'], order['size'], order['id']]) for bid in self._bids: try: # There can be a race condition here, where a price point is removed # between these two ops - thisBid = self._bids[bid] + this_bid = self._bids[bid] except KeyError: continue - for order in thisBid: - result['bids'].append([ - order['price'], - order['size'], - order['id'], - ]) + for order in this_bid: + result['bids'].append([order['price'], order['size'], order['id']]) return result def get_ask(self): diff --git a/gdax/public_client.py b/gdax/public_client.py index fda7f8a9..a95eeb00 100644 --- a/gdax/public_client.py +++ b/gdax/public_client.py @@ -6,66 +6,73 @@ import requests -class PublicClient(): + +class PublicClient(object): def __init__(self, api_url="https://api.gdax.com", product_id="BTC-USD"): self.url = api_url if api_url[-1] == "/": self.url = api_url[:-1] - self.productId = product_id + self.product_id = product_id - def getProducts(self): + def get_products(self): r = requests.get(self.url + '/products') - #r.raise_for_status() + # r.raise_for_status() return r.json() - def getProductOrderBook(self, json=None, level=2, product=''): + def get_product_order_book(self, json=None, level=2, product=''): if type(json) is dict: - if "product" in json: product = json["product"] - if "level" in json: level = json['level'] - r = requests.get(self.url + '/products/%s/book?level=%s' % (product or self.productId, str(level))) - #r.raise_for_status() + if "product" in json: + product = json["product"] + if "level" in json: + level = json['level'] + r = requests.get(self.url + '/products/%s/book?level=%s' % (product or self.product_id, str(level))) + # r.raise_for_status() return r.json() - def getProductTicker(self, json=None, product=''): + def get_product_ticker(self, json=None, product=''): if type(json) is dict: - if "product" in json: product = json["product"] - r = requests.get(self.url + '/products/%s/ticker' % (product or self.productId)) - #r.raise_for_status() + if "product" in json: + product = json["product"] + r = requests.get(self.url + '/products/%s/ticker' % (product or self.product_id)) + # r.raise_for_status() return r.json() - def getProductTrades(self, json=None, product=''): + def get_product_trades(self, json=None, product=''): if type(json) is dict: - if "product" in json: product = json["product"] - r = requests.get(self.url + '/products/%s/trades' % (product or self.productId)) - #r.raise_for_status() + if "product" in json: + product = json["product"] + r = requests.get(self.url + '/products/%s/trades' % (product or self.product_id)) + # r.raise_for_status() return r.json() - def getProductHistoricRates(self, json=None, product='', start='', end='', granularity=''): + def get_product_historic_rates(self, json=None, product='', start='', end='', granularity=''): payload = {} if type(json) is dict: - if "product" in json: product = json["product"] + if "product" in json: + product = json["product"] payload = json else: payload["start"] = start payload["end"] = end payload["granularity"] = granularity - r = requests.get(self.url + '/products/%s/candles' % (product or self.productId), params=payload) - #r.raise_for_status() + r = requests.get(self.url + '/products/%s/candles' % (product or self.product_id), params=payload) + # r.raise_for_status() return r.json() - def getProduct24HrStats(self, json=None, product=''): + def get_product_24hr_stats(self, json=None, product=''): if type(json) is dict: - if "product" in json: product = json["product"] - r = requests.get(self.url + '/products/%s/stats' % (product or self.productId)) - #r.raise_for_status() + if "product" in json: + product = json["product"] + r = requests.get(self.url + '/products/%s/stats' % (product or self.product_id)) + # r.raise_for_status() return r.json() - def getCurrencies(self): + def get_currencies(self): r = requests.get(self.url + '/currencies') - #r.raise_for_status() + # r.raise_for_status() return r.json() - def getTime(self): + def get_time(self): r = requests.get(self.url + '/time') - #r.raise_for_status() + # r.raise_for_status() return r.json() diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index f9fb097c..aded237d 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -6,18 +6,18 @@ from __future__ import print_function import json -import time from threading import Thread from websocket import create_connection + class WebsocketClient(object): - def __init__(self, url=None, products=None, type=None): + def __init__(self, url=None, products=None, message_type="subscribe"): if url is None: url = "wss://ws-feed.gdax.com" self.url = url self.products = products - self.type = "subscribe" #type or "subscribe" + self.type = message_type self.stop = False self.ws = None self.thread = None @@ -27,7 +27,7 @@ def _go(): self._connect() self._listen() - self.onOpen() + self.on_open() self.ws = create_connection(self.url) self.thread = Thread(target=_go) self.thread.start() @@ -53,56 +53,57 @@ def _listen(self): try: msg = json.loads(self.ws.recv()) except Exception as e: - self.onError(e) + self.on_error(e) self.close() else: - self.onMessage(msg) + self.on_message(msg) def close(self): if not self.stop: if self.type == "heartbeat": self.ws.send(json.dumps({"type": "heartbeat", "on": False})) - self.onClose() + self.on_close() self.stop = True - #self.thread = None self.thread.join() self.ws.close() - def onOpen(self): + def on_open(self): print("-- Subscribed! --\n") - def onClose(self): + def on_close(self): print("\n-- Socket Closed --") - def onMessage(self, msg): + def on_message(self, msg): print(msg) - def onError(self, e): + def on_error(self, e): SystemError(e) if __name__ == "__main__": - import GDAX, time - class myWebsocketClient(GDAX.WebsocketClient): - def onOpen(self): + import gdax + import time + + class MyWebsocketClient(gdax.WebsocketClient): + def on_open(self): self.url = "wss://ws-feed.gdax.com/" self.products = ["BTC-USD", "ETH-USD"] - self.MessageCount = 0 + self.message_count = 0 print("Let's count the messages!") - def onMessage(self, msg): + def on_message(self, msg): if 'price' in msg and 'type' in msg: print("Message type:", msg["type"], "\t@ %.3f" % float(msg["price"])) - self.MessageCount += 1 + self.message_count += 1 - def onClose(self): - print ("-- Goodbye! --") + def on_close(self): + print("-- Goodbye! --") - wsClient = myWebsocketClient() + wsClient = MyWebsocketClient() wsClient.start() print(wsClient.url, wsClient.products) # Do some logic with the data - while (wsClient.MessageCount < 500): - print("\nMessageCount =", "%i \n" % wsClient.MessageCount) + while wsClient.message_count < 500: + print("\nMessageCount =", "%i \n" % wsClient.message_count) time.sleep(1) wsClient.close() diff --git a/setup.py b/setup.py index ebc11347..a65e3480 100644 --- a/setup.py +++ b/setup.py @@ -10,17 +10,17 @@ ] setup( - name = 'GDAX', - version = '0.3.1', - author = 'Daniel Paquin', - author_email = 'dpaq34@gmail.com', + name='GDAX', + version='0.3.1', + author='Daniel Paquin', + author_email='dpaq34@gmail.com', license='MIT', - url = 'https://github.com/danpaquin/GDAX-Python', - packages = find_packages(), - install_requires = install_requires, - description = 'The unofficial Python client for the GDAX API', - download_url = 'https://github.com/danpaquin/GDAX-Python/archive/master.zip', - keywords = ['gdax', 'gdax-api', 'orderbook', 'trade', 'bitcoin', 'ethereum', 'BTC', 'ETH', 'client', 'api', 'wrapper', 'exchange', 'crypto', 'currency', 'trading', 'trading-api', 'coinbase'], + url='https://github.com/danpaquin/GDAX-Python', + packages=find_packages(), + install_requires=install_requires, + description='The unofficial Python client for the GDAX API', + download_url='https://github.com/danpaquin/GDAX-Python/archive/master.zip', + keywords=['gdax', 'gdax-api', 'orderbook', 'trade', 'bitcoin', 'ethereum', 'BTC', 'ETH', 'client', 'api', 'wrapper', 'exchange', 'crypto', 'currency', 'trading', 'trading-api', 'coinbase'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', From 26554284568075e965a475222c82713b37525c33 Mon Sep 17 00:00:00 2001 From: acontry Date: Sat, 10 Jun 2017 18:05:40 -0700 Subject: [PATCH 030/174] Clean up class initialization and inheritance. --- gdax/authenticated_client.py | 5 +---- gdax/order_book.py | 2 +- gdax/public_client.py | 4 +--- gdax/websocket_client.py | 5 +---- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index 7c56d9ab..508a97ca 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -16,10 +16,7 @@ class AuthenticatedClient(PublicClient): def __init__(self, key, b64secret, passphrase, api_url="https://api.gdax.com", product_id="BTC-USD"): - self.url = api_url - if api_url[-1] == "/": - self.url = api_url[:-1] - self.product_id = product_id + super(self.__class__, self).__init__(api_url, product_id) self.auth = GdaxAuth(key, b64secret, passphrase) def get_account(self, account_id): diff --git a/gdax/order_book.py b/gdax/order_book.py index 012aa7dc..eb418c8e 100644 --- a/gdax/order_book.py +++ b/gdax/order_book.py @@ -14,7 +14,7 @@ class OrderBook(WebsocketClient): def __init__(self, product_id='BTC-USD'): - WebsocketClient.__init__(self, products=product_id) + super(self.__class__, self).__init__(products=product_id) self._asks = RBTree() self._bids = RBTree() self._client = PublicClient(product_id=product_id) diff --git a/gdax/public_client.py b/gdax/public_client.py index a95eeb00..2bcc7977 100644 --- a/gdax/public_client.py +++ b/gdax/public_client.py @@ -9,9 +9,7 @@ class PublicClient(object): def __init__(self, api_url="https://api.gdax.com", product_id="BTC-USD"): - self.url = api_url - if api_url[-1] == "/": - self.url = api_url[:-1] + self.url = api_url.rstrip("/") self.product_id = product_id def get_products(self): diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index aded237d..8e8b4c2a 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -11,10 +11,7 @@ class WebsocketClient(object): - def __init__(self, url=None, products=None, message_type="subscribe"): - if url is None: - url = "wss://ws-feed.gdax.com" - + def __init__(self, url="wss://ws-feed.gdax.com", products=None, message_type="subscribe"): self.url = url self.products = products self.type = message_type From 31e015c67c2c5e4841041818a3558afc537c1109 Mon Sep 17 00:00:00 2001 From: acontry Date: Sat, 10 Jun 2017 18:28:40 -0700 Subject: [PATCH 031/174] Fix files to match lowercase. --- gdax/__init__.py | 8 ++++---- gdax/authenticated_client.py | 2 +- gdax/order_book.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gdax/__init__.py b/gdax/__init__.py index 8f0b656d..cb5fda32 100644 --- a/gdax/__init__.py +++ b/gdax/__init__.py @@ -1,4 +1,4 @@ -from gdax.AuthenticatedClient import AuthenticatedClient -from gdax.PublicClient import PublicClient -from gdax.WebsocketClient import WebsocketClient -from gdax.OrderBook import OrderBook +from gdax.authenticated_client import AuthenticatedClient +from gdax.public_client import PublicClient +from gdax.websocket_client import WebsocketClient +from gdax.order_book import OrderBook diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index 508a97ca..adb8e99b 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -11,7 +11,7 @@ import base64 import json from requests.auth import AuthBase -from gdax.PublicClient import PublicClient +from gdax.public_client import PublicClient class AuthenticatedClient(PublicClient): diff --git a/gdax/order_book.py b/gdax/order_book.py index eb418c8e..b0ef7ec8 100644 --- a/gdax/order_book.py +++ b/gdax/order_book.py @@ -8,8 +8,8 @@ from bintrees import RBTree from decimal import Decimal -from gdax.PublicClient import PublicClient -from gdax.WebsocketClient import WebsocketClient +from gdax.public_client import PublicClient +from gdax.websocket_client import WebsocketClient class OrderBook(WebsocketClient): From 02e48db9ff04b2dc792cdad8591a05e713381fa8 Mon Sep 17 00:00:00 2001 From: acontry Date: Mon, 12 Jun 2017 21:40:51 -0700 Subject: [PATCH 032/174] Update readme to match API changes. --- README.md | 110 +++++++++++++++++++++++++++--------------------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 92c6490b..9157aa9f 100644 --- a/README.md +++ b/README.md @@ -30,62 +30,62 @@ pip install GDAX Only some endpoints in the API are available to everyone. The public endpoints can be reached using ```PublicClient``` ```python -import GDAX -publicClient = GDAX.PublicClient() +import gdax +public_client = gdax.PublicClient() # Set a default product -publicClient = GDAX.PublicClient(product_id="ETH-USD") +public_client = gdax.PublicClient(product_id="ETH-USD") ``` ### PublicClient Methods - [getProducts](https://docs.gdax.com/#get-products) ```python -publicClient.getProducts() +public_client.get_products() ``` - [getProductOrderBook](https://docs.gdax.com/#get-product-order-book) ```python # Get the order book at the default level. -publicClient.getProductOrderBook() -# Get the order book at a specfific level. -publicClient.getProductOrderBook(level=1) +public_client.get_product_order_book() +# Get the order book at a specific level. +public_client.get_product_order_book(level=1) ``` - [getProductTicker](https://docs.gdax.com/#get-product-ticker) ```python # Get the product ticker for the default product. -publicClient.getProductTicker() +public_client.get_product_ticker() # Get the product ticker for a specific product. -publicClient.getProductTicker(product="ETH-USD") +public_client.get_product_ticker(product="ETH-USD") ``` - [getProductTrades](https://docs.gdax.com/#get-trades) ```python # Get the product trades for the default product. -publicClient.getProductTrades() +public_client.get_product_trades() # Get the product trades for a specific product. -publicClient.getProductTrades(product="ETH-USD") +public_client.get_product_trades(product="ETH-USD") ``` - [getProductHistoricRates](https://docs.gdax.com/#get-historic-rates) ```python -publicClient.getProductHistoricRates() +public_client.get_product_historic_rates() # To include other parameters, see official documentation: -publicClient.getProductHistoricRates(granularity=3000) +public_client.get_product_historic_rates(granularity=3000) ``` - [getProduct24HrStates](https://docs.gdax.com/#get-24hr-stats) ```python -publicClient.getProduct24HrStats() +public_client.get_product_24hr_stats() ``` - [getCurrencies](https://docs.gdax.com/#get-currencies) ```python -publicClient.getCurrencies() +public_client.get_currencies() ``` - [getTime](https://docs.gdax.com/#time) ```python -publicClient.getTime() +public_client.get_time() ``` #### *In Development* JSON Parsing @@ -93,15 +93,15 @@ Only available for the `PublicClient`, you may pass any function above raw JSON - Both of these calls send the same request: ```python -import GDAX -publicClient = GDAX.PublicClient() +import gdax +public_client = gdax.PublicClient() -method1 = publicClient.getProductHistoricRates(granularity='3000') +method1 = public_client.get_product_historic_rates(granularity='3000') params = { 'granularity': '3000' } -method2 = publicClient.getProductHistoricRates(params) +method2 = public_client.get_product_historic_rates(params) # Both methods will send the same request, but not always return the same data if run in series. print (method1, method2) @@ -120,18 +120,18 @@ class, so you will only need to initialize one if you are planning to integrate both into your script. ```python -import GDAX -authClient = GDAX.AuthenticatedClient(key, b64secret, passphrase) +import gdax +auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase) # Set a default product -authClient = GDAX.AuthenticatedClient(key, b64secret, passphrase, product_id="ETH-USD") -# Use the sandbox API (requires a different set of API access crudentials) -authClient = GDAX.AuthenticatedClient(key, b64secret, passphrase, api_url="https://api-public.sandbox.gdax.com") +auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase, product_id="ETH-USD") +# Use the sandbox API (requires a different set of API access credentials) +auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase, api_url="https://api-public.sandbox.gdax.com") ``` ### Pagination Some calls are [paginated](https://docs.gdax.com/#pagination), meaning multiple calls must be made to receive the full set of data. Each page/request is a list of dict objects that are then appended to a master list, making it easy to navigate pages (e.g. ```request[0]``` would return the first page of data in the example below). *This feature is under consideration for redesign. Please provide feedback if you have issues or suggestions* ```python -request = authClient.getFills(limit=100) +request = auth_client.get_fills(limit=100) request[0] # Page 1 always present request[1] # Page 2+ present only if the data exists ``` @@ -140,64 +140,64 @@ It should be noted that limit does not behave exactly as the official documentat ### AuthenticatedClient Methods - [getAccounts](https://docs.gdax.com/#list-accounts) ```python -authClient.getAccounts() +auth_client.get_accounts() ``` - [getAccount](https://docs.gdax.com/#get-an-account) ```python -authClient.getAccount("7d0f7d8e-dd34-4d9c-a846-06f431c381ba") +auth_client.get_account("7d0f7d8e-dd34-4d9c-a846-06f431c381ba") ``` - [getAccountHistory](https://docs.gdax.com/#get-account-history) (paginated) ```python -authClient.getAccountHistory("7d0f7d8e-dd34-4d9c-a846-06f431c381ba") +auth_client.get_account_history("7d0f7d8e-dd34-4d9c-a846-06f431c381ba") ``` - [getAccountHolds](https://docs.gdax.com/#get-holds) (paginated) ```python -authClient.getAccountHolds("7d0f7d8e-dd34-4d9c-a846-06f431c381ba") +auth_client.get_account_holds("7d0f7d8e-dd34-4d9c-a846-06f431c381ba") ``` - [buy & sell](https://docs.gdax.com/#place-a-new-order) ```python # Buy 0.01 BTC @ 100 USD -authClient.buy(price='100.00', #USD +auth_client.buy(price='100.00', #USD size='0.01', #BTC product_id='BTC-USD') ``` ```python # Sell 0.01 BTC @ 200 USD -authClient.sell(price='200.00', #USD +auth_client.sell(price='200.00', #USD size='0.01', #BTC product_id='BTC-USD') ``` - [cancelOrder](https://docs.gdax.com/#cancel-an-order) ```python -authClient.cancelOrder("d50ec984-77a8-460a-b958-66f114b0de9b") +auth_client.cancel_order("d50ec984-77a8-460a-b958-66f114b0de9b") ``` - [cancelAll](https://docs.gdax.com/#cancel-all) ```python -authClient.cancelAll(product='BTC-USD') +auth_client.cancel_all(product='BTC-USD') ``` - [getOrders](https://docs.gdax.com/#list-orders) (paginated) ```python -authClient.getOrders() +auth_client.get_orders() ``` - [getOrder](https://docs.gdax.com/#get-an-order) ```python -authClient.getOrder("d50ec984-77a8-460a-b958-66f114b0de9b") +auth_client.get_order("d50ec984-77a8-460a-b958-66f114b0de9b") ``` - [getFills](https://docs.gdax.com/#list-fills) (paginated) ```python -authClient.getFills() +auth_client.get_fills() # Get fills for a specific order -authClient.getFills(orderId="d50ec984-77a8-460a-b958-66f114b0de9b") +auth_client.get_fills(orderId="d50ec984-77a8-460a-b958-66f114b0de9b") # Get fills for a specific product -authClient.getFills(productId="ETH-BTC") +auth_client.get_fills(productId="ETH-BTC") ``` - [deposit & withdraw](https://docs.gdax.com/#depositwithdraw) @@ -207,7 +207,7 @@ depositParams = { 'amount': '25.00', # Currency determined by account specified 'coinbase_account_id': '60680c98bfe96c2601f27e9c' } -authClient.deposit(depositParams) +auth_client.deposit(depositParams) ``` ```python # Withdraw from GDAX into Coinbase Wallet @@ -215,7 +215,7 @@ withdrawParams = { 'amount': '1.00', # Currency determined by account specified 'coinbase_account_id': '536a541fa9393bb3c7000023' } -authClient.withdraw(withdrawParams) +auth_client.withdraw(withdrawParams) ``` ### WebsocketClient @@ -223,18 +223,18 @@ If you would like to receive real-time market updates, you must subscribe to the #### Subscribe to a single product ```python -import GDAX +import gdax # Paramters are optional -wsClient = GDAX.WebsocketClient(url="wss://ws-feed.gdax.com", products="BTC-USD") +wsClient = gdax.WebsocketClient(url="wss://ws-feed.gdax.com", products="BTC-USD") # Do other stuff... wsClient.close() ``` #### Subscribe to multiple products ```python -import GDAX +import gdax # Paramters are optional -wsClient = GDAX.WebsocketClient(url="wss://ws-feed.gdax.com", products=["BTC-USD", "ETH-USD"]) +wsClient = gdax.WebsocketClient(url="wss://ws-feed.gdax.com", products=["BTC-USD", "ETH-USD"]) # Do other stuff... wsClient.close() ``` @@ -247,25 +247,25 @@ The ```WebsocketClient``` subscribes in a separate thread upon initialization. - onClose - called once after the websocket has been closed. - close - call this method to close the websocket connection (do not overwrite). ```python -import GDAX, time -class myWebsocketClient(GDAX.WebsocketClient): - def onOpen(self): +import gdax, time +class myWebsocketClient(gdax.WebsocketClient): + def on_open(self): self.url = "wss://ws-feed.gdax.com/" self.products = ["LTC-USD"] - self.MessageCount = 0 + self.message_count = 0 print("Lets count the messages!") - def onMessage(self, msg): + def on_message(self, msg): self.MessageCount += 1 if 'price' in msg and 'type' in msg: print ("Message type:", msg["type"], "\t@ {}.3f".format(float(msg["price"]))) - def onClose(self): + def on_close(self): print("-- Goodbye! --") wsClient = myWebsocketClient() wsClient.start() print(wsClient.url, wsClient.products) -while (wsClient.MessageCount < 500): - print ("\nMessageCount =", "{} \n".format(wsClient.MessageCount)) +while (wsClient.message_count < 500): + print ("\nMessageCount =", "{} \n".format(wsClient.message_count)) time.sleep(1) wsClient.close() ``` @@ -274,8 +274,8 @@ wsClient.close() The ```OrderBook``` subscribes to a websocket and keeps a real-time record of the orderbook for the product_id input. Please provide your feedback for future improvements. ```python -import GDAX, time -order_book = GDAX.OrderBook(product_id='BTC-USD') +import gdax, time +order_book = gdax.OrderBook(product_id='BTC-USD') order_book.start() time.sleep(10) order_book.close() From 1840880e48935356dc320ebc730e048a7bb8f5f2 Mon Sep 17 00:00:00 2001 From: Ted McCormack Date: Tue, 13 Jun 2017 04:33:16 -0700 Subject: [PATCH 033/174] Remove ticker, added it to orderbook --- GDAX/OrderBook.py | 64 ++++++++++++++++++++++------------------- GDAX/Ticker.py | 42 --------------------------- GDAX/WebsocketClient.py | 5 +++- 3 files changed, 39 insertions(+), 72 deletions(-) delete mode 100644 GDAX/Ticker.py diff --git a/GDAX/OrderBook.py b/GDAX/OrderBook.py index 66e2c330..5c6b5595 100644 --- a/GDAX/OrderBook.py +++ b/GDAX/OrderBook.py @@ -12,11 +12,10 @@ from GDAX.PublicClient import PublicClient from GDAX.WebsocketClient import WebsocketClient - class OrderBook(WebsocketClient): - def __init__(self, product_id='BTC-USD', log_to=None): - WebsocketClient.__init__(self, products=product_id) + def __init__(self, url=None, product_id='BTC-USD', live=True, log_to=None): + WebsocketClient.__init__(self, url=url, products=product_id) self._asks = RBTree() self._bids = RBTree() self._client = PublicClient(product_id=product_id) @@ -24,38 +23,41 @@ def __init__(self, product_id='BTC-USD', log_to=None): if log_to: assert hasattr(log_to, 'write') self._log_to = log_to + self._current_ticker = None + self.__live = live def onMessage(self, message): if self._log_to: pickle.dump(message, self._log_to) sequence = message['sequence'] - if self._sequence == -1: - self._asks = RBTree() - self._bids = RBTree() - res = self._client.getProductOrderBook(level=3) - for bid in res['bids']: - self.add({ - 'id': bid[2], - 'side': 'buy', - 'price': Decimal(bid[0]), - 'size': Decimal(bid[1]) - }) - for ask in res['asks']: - self.add({ - 'id': ask[2], - 'side': 'sell', - 'price': Decimal(ask[0]), - 'size': Decimal(ask[1]) - }) - self._sequence = res['sequence'] - - if sequence <= self._sequence: - return #ignore old messages - elif sequence > self._sequence + 1: - self.close() - self.start() - return + if self.__live: + if self._sequence == -1: + self._asks = RBTree() + self._bids = RBTree() + res = self._client.getProductOrderBook(level=3) + for bid in res['bids']: + self.add({ + 'id': bid[2], + 'side': 'buy', + 'price': Decimal(bid[0]), + 'size': Decimal(bid[1]) + }) + for ask in res['asks']: + self.add({ + 'id': ask[2], + 'side': 'sell', + 'price': Decimal(ask[0]), + 'size': Decimal(ask[1]) + }) + self._sequence = res['sequence'] + + if sequence <= self._sequence: + return #ignore old messages + elif sequence > self._sequence + 1: + self.close() + self.start() + return msg_type = message['type'] if msg_type == 'open': @@ -64,6 +66,7 @@ def onMessage(self, message): self.remove(message) elif msg_type == 'match': self.match(message) + self._current_ticker = message elif msg_type == 'change': self.change(message) @@ -168,6 +171,9 @@ def change(self, order): if node is None or not any(o['id'] == order['order_id'] for o in node): return + def get_current_ticker(self): + return self._current_ticker + def get_current_book(self): result = dict() result['sequence'] = self._sequence diff --git a/GDAX/Ticker.py b/GDAX/Ticker.py deleted file mode 100644 index c3890929..00000000 --- a/GDAX/Ticker.py +++ /dev/null @@ -1,42 +0,0 @@ -# -# GDAX/OrderBook.py -# David Caseria -# -# Live order book updated from the GDAX Websocket Feed - -from GDAX.PublicClient import PublicClient -from GDAX.WebsocketClient import WebsocketClient - -class Ticker(WebsocketClient): - - def __init__(self, product_id='BTC-USD', log_to=None): - ticker_url = PublicClient( - api_url='wss://ws-feed.gdax.com', - product_id=product_id, - url_only=True - ).getProductTicker() - - WebsocketClient.__init__(self, url=ticker_url, products=product_id) - - if log_to: - assert hasattr(log_to, 'write') - self._log_to = log_to - self._current_ticker = None - - def onMessage(self, message): - if 'type' in message and message['type'] == 'match': - self._current_ticker = message - if self._log_to: - pickle.dump(message, self._log_to) - - def get_current_ticker(self): - return self._current_ticker - -if __name__ == '__main__': - import time - ticker = Ticker() - ticker.start() - while True: - print(ticker.get_current_ticker()) - time.sleep(10) - ticker.close() diff --git a/GDAX/WebsocketClient.py b/GDAX/WebsocketClient.py index 94596ec0..ba8f15d4 100644 --- a/GDAX/WebsocketClient.py +++ b/GDAX/WebsocketClient.py @@ -7,6 +7,7 @@ from __future__ import print_function import json import time +import traceback from threading import Thread from websocket import create_connection @@ -53,8 +54,10 @@ def _listen(self): try: msg = json.loads(self.ws.recv()) except Exception as e: + traceback.print_exc() self.onError(e) self.close() + self.start() else: self.onMessage(msg) @@ -78,7 +81,7 @@ def onMessage(self, msg): print(msg) def onError(self, e): - SystemError(e) + return if __name__ == "__main__": From 6200e50d99e845271a9ebb0aebe4eb88a413285e Mon Sep 17 00:00:00 2001 From: Ted McCormack Date: Tue, 13 Jun 2017 04:36:58 -0700 Subject: [PATCH 034/174] Ticker now from orderbook --- GDAX/OrderBook.py | 5 ----- GDAX/__init__.py | 1 - 2 files changed, 6 deletions(-) diff --git a/GDAX/OrderBook.py b/GDAX/OrderBook.py index 9750edf1..5520322e 100644 --- a/GDAX/OrderBook.py +++ b/GDAX/OrderBook.py @@ -13,14 +13,9 @@ from GDAX.WebsocketClient import WebsocketClient class OrderBook(WebsocketClient): -<<<<<<< HEAD def __init__(self, url=None, product_id='BTC-USD', live=True, log_to=None): WebsocketClient.__init__(self, url=url, products=product_id) -======= - def __init__(self, product_id='BTC-USD'): - WebsocketClient.__init__(self, products=product_id) ->>>>>>> upstream/master self._asks = RBTree() self._bids = RBTree() self._client = PublicClient(product_id=product_id) diff --git a/GDAX/__init__.py b/GDAX/__init__.py index d00382bd..03d6b8a9 100644 --- a/GDAX/__init__.py +++ b/GDAX/__init__.py @@ -2,4 +2,3 @@ from GDAX.PublicClient import PublicClient from GDAX.WebsocketClient import WebsocketClient from GDAX.OrderBook import OrderBook -from GDAX.Ticker import Ticker From 5fee1138ecb8b9cb52e66c2da664dfb6f2f7921a Mon Sep 17 00:00:00 2001 From: Jiang Bian Date: Tue, 13 Jun 2017 23:47:05 -0400 Subject: [PATCH 035/174] change GDAX to gdax in setup.py and correct spelling in README.md because of recent pep8 refactor --- README.md | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9157aa9f..3c44787f 100644 --- a/README.md +++ b/README.md @@ -195,9 +195,9 @@ auth_client.get_order("d50ec984-77a8-460a-b958-66f114b0de9b") ```python auth_client.get_fills() # Get fills for a specific order -auth_client.get_fills(orderId="d50ec984-77a8-460a-b958-66f114b0de9b") +auth_client.get_fills(order_id="d50ec984-77a8-460a-b958-66f114b0de9b") # Get fills for a specific product -auth_client.get_fills(productId="ETH-BTC") +auth_client.get_fills(product_id="ETH-BTC") ``` - [deposit & withdraw](https://docs.gdax.com/#depositwithdraw) diff --git a/setup.py b/setup.py index a65e3480..1d028f5b 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ ] setup( - name='GDAX', + name='gdax', version='0.3.1', author='Daniel Paquin', author_email='dpaq34@gmail.com', From 79f99f25f9a3c40693a10947435928f19fd83dc8 Mon Sep 17 00:00:00 2001 From: acontry Date: Tue, 13 Jun 2017 22:28:56 -0700 Subject: [PATCH 036/174] Clean up PublicClient. Add doctrings + tests. Drop the mixed keyword/dict interface in favor of keywords only. Add docstrings based on Google format. --- README.md | 119 +++++++++++------ gdax/public_client.py | 247 ++++++++++++++++++++++++++++++------ setup.py | 5 + tests/test_public_client.py | 52 ++++++++ 4 files changed, 345 insertions(+), 78 deletions(-) create mode 100644 tests/test_public_client.py diff --git a/README.md b/README.md index 3c44787f..1c85c98f 100644 --- a/README.md +++ b/README.md @@ -2,38 +2,49 @@ The Python client for the [GDAX API](https://docs.gdax.com/) (formerly known as the Coinbase Exchange API) ##### Provided under MIT License by Daniel Paquin. -*Note: this library may be subtly broken or buggy. The code is released under the MIT License – please take the following message to heart:* -> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*Note: this library may be subtly broken or buggy. The code is released under +the MIT License – please take the following message to heart:* +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## Benefits - A simple to use python wrapper for both public and authenticated endpoints. -- In about 10 minutes, you could be programmatically trading on one of the largest Bitcoin exchanges in the *world*! -- Do not worry about handling the nuances of the API with easy-to-use methods for every API endpoint. -- Gain an advantage in the market by getting under the hood of GDAX to learn what and who is *really* behind every tick. +- In about 10 minutes, you could be programmatically trading on one of the +largest Bitcoin exchanges in the *world*! +- Do not worry about handling the nuances of the API with easy-to-use methods +for every API endpoint. +- Gain an advantage in the market by getting under the hood of GDAX to learn +what and who is *really* behind every tick. ## Under Development -- Test Scripts **on dev branch** -- Additional Functionality for *WebsocketClient*, including a real-time order book +- Unit testing +- Additional Functionality for *WebsocketClient*, including a real-time order +book - FIX API Client **Looking for support** ## Getting Started -This README is documentation on the syntax of the python client presented in this repository. **In order to use this wrapper to its full potential, you must familiarize yourself with the official GDAX documentation.** +This README is documentation on the syntax of the python client presented in +this repository. **In order to use this wrapper to its full potential, you must +familiarize yourself with the official GDAX documentation.** - https://docs.gdax.com/ - You may manually install the project or use ```pip```: ```python -pip install GDAX +pip install gdax ``` ### Public Client -Only some endpoints in the API are available to everyone. The public endpoints can be reached using ```PublicClient``` +Only some endpoints in the API are available to everyone. The public endpoints +can be reached using ```PublicClient``` ```python import gdax public_client = gdax.PublicClient() -# Set a default product -public_client = gdax.PublicClient(product_id="ETH-USD") ``` ### PublicClient Methods @@ -45,37 +56,33 @@ public_client.get_products() - [getProductOrderBook](https://docs.gdax.com/#get-product-order-book) ```python # Get the order book at the default level. -public_client.get_product_order_book() +public_client.get_product_order_book(product_id='BTC-USD') # Get the order book at a specific level. -public_client.get_product_order_book(level=1) +public_client.get_product_order_book(product_id='BTC-USD', level=3) ``` - [getProductTicker](https://docs.gdax.com/#get-product-ticker) ```python -# Get the product ticker for the default product. -public_client.get_product_ticker() # Get the product ticker for a specific product. -public_client.get_product_ticker(product="ETH-USD") +public_client.get_product_ticker(product_id='ETH-USD') ``` - [getProductTrades](https://docs.gdax.com/#get-trades) ```python -# Get the product trades for the default product. -public_client.get_product_trades() # Get the product trades for a specific product. -public_client.get_product_trades(product="ETH-USD") +public_client.get_product_trades(product_id='ETH-USD') ``` - [getProductHistoricRates](https://docs.gdax.com/#get-historic-rates) ```python -public_client.get_product_historic_rates() +public_client.get_product_historic_rates(product_id='ETH-USD') # To include other parameters, see official documentation: -public_client.get_product_historic_rates(granularity=3000) +public_client.get_product_historic_rates('ETH-USD', granularity=3000) ``` - [getProduct24HrStates](https://docs.gdax.com/#get-24hr-stats) ```python -public_client.get_product_24hr_stats() +public_client.get_product_24hr_stats('ETH-USD') ``` - [getCurrencies](https://docs.gdax.com/#get-currencies) @@ -88,8 +95,9 @@ public_client.get_currencies() public_client.get_time() ``` -#### *In Development* JSON Parsing -Only available for the `PublicClient`, you may pass any function above raw JSON data. This may be useful for some applications of the project and should not hinder performance, but we are looking into this. *Do you love or hate this? Please share your thoughts within the issue tab!* +#### Function Call Methods +The API presents a consistent interface for parameters, which may be specified +in different ways. - Both of these calls send the same request: ```python @@ -98,12 +106,11 @@ public_client = gdax.PublicClient() method1 = public_client.get_product_historic_rates(granularity='3000') -params = { -'granularity': '3000' -} -method2 = public_client.get_product_historic_rates(params) +params = {'granularity': '3000'} +method2 = public_client.get_product_historic_rates(**params) -# Both methods will send the same request, but not always return the same data if run in series. +# Both methods will send the same request, but not always return the same data +# if run in series because of real-time data changes from GDAX. print (method1, method2) ``` @@ -123,19 +130,29 @@ integrate both into your script. import gdax auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase) # Set a default product -auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase, product_id="ETH-USD") +auth_client = gdax.AuthenticatedClient(key, b64secret, + passphrase, product_id="ETH-USD") # Use the sandbox API (requires a different set of API access credentials) -auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase, api_url="https://api-public.sandbox.gdax.com") +auth_client = gdax.AuthenticatedClient(key, b64secret, + passphrase, api_url="https://api-public.sandbox.gdax.com") ``` ### Pagination -Some calls are [paginated](https://docs.gdax.com/#pagination), meaning multiple calls must be made to receive the full set of data. Each page/request is a list of dict objects that are then appended to a master list, making it easy to navigate pages (e.g. ```request[0]``` would return the first page of data in the example below). *This feature is under consideration for redesign. Please provide feedback if you have issues or suggestions* +Some calls are [paginated](https://docs.gdax.com/#pagination), meaning multiple +calls must be made to receive the full set of data. Each page/request is a list +of dict objects that are then appended to a master list, making it easy to +navigate pages (e.g. ```request[0]``` would return the first page of data in the +example below). *This feature is under consideration for redesign. Please +provide feedback if you have issues or suggestions* ```python request = auth_client.get_fills(limit=100) request[0] # Page 1 always present request[1] # Page 2+ present only if the data exists ``` -It should be noted that limit does not behave exactly as the official documentation specifies. If you request a limit and that limit is met, additional pages will not be returned. This is to ensure speedy response times when less data is prefered. +It should be noted that limit does not behave exactly as the official +documentation specifies. If you request a limit and that limit is met, +additional pages will not be returned. This is to ensure speedy response times +when less data is preferred. ### AuthenticatedClient Methods - [getAccounts](https://docs.gdax.com/#list-accounts) @@ -240,11 +257,16 @@ wsClient.close() ``` ### WebsocketClient Methods -The ```WebsocketClient``` subscribes in a separate thread upon initialization. There are three methods which you could overwrite (before initialization) so it can react to the data streaming in. The current client is a template used for illustration purposes only. - -- onOpen - called once, *immediately before* the socket connection is made, this is where you want to add inital parameters. -- onMessage - called once for every message that arrives and accepts one argument that contains the message of dict type. -- onClose - called once after the websocket has been closed. +The ```WebsocketClient``` subscribes in a separate thread upon initialization. +There are three methods which you could overwrite (before initialization) so it +can react to the data streaming in. The current client is a template used for +illustration purposes only. + +- on_open - called once, *immediately before* the socket connection is made, this +is where you want to add inital parameters. +- on_message - called once for every message that arrives and accepts one +argument that contains the message of dict type. +- on_close - called once after the websocket has been closed. - close - call this method to close the websocket connection (do not overwrite). ```python import gdax, time @@ -255,7 +277,7 @@ class myWebsocketClient(gdax.WebsocketClient): self.message_count = 0 print("Lets count the messages!") def on_message(self, msg): - self.MessageCount += 1 + self.Message_count += 1 if 'price' in msg and 'type' in msg: print ("Message type:", msg["type"], "\t@ {}.3f".format(float(msg["price"]))) def on_close(self): @@ -271,7 +293,9 @@ wsClient.close() ``` ### Real-time OrderBook -The ```OrderBook``` subscribes to a websocket and keeps a real-time record of the orderbook for the product_id input. Please provide your feedback for future improvements. +The ```OrderBook``` subscribes to a websocket and keeps a real-time record of +the orderbook for the product_id input. Please provide your feedback for +future improvements. ```python import gdax, time @@ -281,6 +305,16 @@ time.sleep(10) order_book.close() ``` +### Testing +Unit tests are under development using the pytest framework. Contributions are +welcome! + +To run the full test suite, in the project +directory run: +```bash +python -m pytest +``` + ## Change Log *0.3* **Current PyPI release** - Added crypto and LTC deposit & withdraw (undocumented). @@ -293,7 +327,8 @@ order_book.close() - Added additional API functionality such as cancelAll() and ETH withdrawal. *0.2.1* -- Allowed ```WebsocketClient``` to operate intuitively and restructured example workflow. +- Allowed ```WebsocketClient``` to operate intuitively and restructured +example workflow. *0.2.0* - Renamed project to GDAX-Python diff --git a/gdax/public_client.py b/gdax/public_client.py index 5742b9d8..ecbc7880 100644 --- a/gdax/public_client.py +++ b/gdax/public_client.py @@ -8,69 +8,244 @@ class PublicClient(object): - def __init__(self, api_url="https://api.gdax.com", product_id="BTC-USD"): - self.url = api_url.rstrip("/") - self.product_id = product_id + """GDAX public client API. + + All requests default to the `product_id` specified at object + creation if not otherwise specified. + + Attributes: + url (Optional[str]): API URL. Defaults to GDAX API. + + """ + + def __init__(self, api_url='https://api.gdax.com'): + """Create GDAX API public client. + + Args: + api_url (Optional[str]): API URL. Defaults to GDAX API. + + """ + self.url = api_url.rstrip('/') def get_products(self): + """Get a list of available currency pairs for trading. + + Returns: + list: Info about all currency pairs. Example:: + [ + { + "id": "BTC-USD", + "display_name": "BTC/USD", + "base_currency": "BTC", + "quote_currency": "USD", + "base_min_size": "0.01", + "base_max_size": "10000.00", + "quote_increment": "0.01" + } + ] + + """ r = requests.get(self.url + '/products') # r.raise_for_status() return r.json() - def get_product_order_book(self, json=None, level=2, product=''): - if type(json) is dict: - if "product" in json: - product = json["product"] - if "level" in json: - level = json['level'] - r = requests.get(self.url + '/products/{}/book?level={}'.format(product or self.product_id, str(level))) + def get_product_order_book(self, product_id, level=1): + """Get a list of open orders for a product. + + The amount of detail shown can be customized with the `level` + parameter: + * 1: Only the best bid and ask + * 2: Top 50 bids and asks (aggregated) + * 3: Full order book (non aggregated) + + Level 1 and Level 2 are recommended for polling. For the most + up-to-date data, consider using the websocket stream. + + **Caution**: Level 3 is only recommended for users wishing to + maintain a full real-time order book using the websocket + stream. Abuse of Level 3 via polling will cause your access to + be limited or blocked. + + Args: + product_id (str): Product + level (Optional[int]): Order book level (1, 2, or 3). + Default is 1. + + Returns: + dict: Order book. Example for level 1:: + { + "sequence": "3", + "bids": [ + [ price, size, num-orders ], + ], + "asks": [ + [ price, size, num-orders ], + ] + } + + """ + params = {'level': level} + r = requests.get(self.url + '/products/{}/book' + .format(product_id), params=params) # r.raise_for_status() return r.json() - def get_product_ticker(self, json=None, product=''): - if type(json) is dict: - if "product" in json: - product = json["product"] - r = requests.get(self.url + '/products/{}/ticker'.format(product or self.product_id)) + def get_product_ticker(self, product_id): + """Snapshot about the last trade (tick), best bid/ask and 24h volume. + + **Caution**: Polling is discouraged in favor of connecting via + the websocket stream and listening for match messages. + + Args: + product_id (str): Product + + Returns: + dict: Ticker info. Example:: + { + "trade_id": 4729088, + "price": "333.99", + "size": "0.193", + "bid": "333.98", + "ask": "333.99", + "volume": "5957.11914015", + "time": "2015-11-14T20:46:03.511254Z" + } + + """ + r = requests.get(self.url + '/products/{}/ticker' + .format(product_id)) # r.raise_for_status() return r.json() - def get_product_trades(self, json=None, product=''): - if type(json) is dict: - if "product" in json: - product = json["product"] - r = requests.get(self.url + '/products/{}/trades'.format(product or self.product_id)) + def get_product_trades(self, product_id): + """List the latest trades for a product. + + Args: + product_id (str): Product + + Returns: + list: Latest trades. Example:: + [{ + "time": "2014-11-07T22:19:28.578544Z", + "trade_id": 74, + "price": "10.00000000", + "size": "0.01000000", + "side": "buy" + }, { + "time": "2014-11-07T01:08:43.642366Z", + "trade_id": 73, + "price": "100.00000000", + "size": "0.01000000", + "side": "sell" + }] + + """ + r = requests.get(self.url + '/products/{}/trades'.format(product_id)) # r.raise_for_status() return r.json() - def get_product_historic_rates(self, json=None, product='', start='', end='', granularity=''): - payload = {} - if type(json) is dict: - if "product" in json: - product = json["product"] - payload = json - else: - payload["start"] = start - payload["end"] = end - payload["granularity"] = granularity - r = requests.get(self.url + '/products/{}/candles'.format(product or self.product_id), params=payload) + def get_product_historic_rates(self, product_id, start=None, end=None, + granularity=None): + """Historic rates for a product. + + Rates are returned in grouped buckets based on requested + `granularity`. If start, end, and granularity aren't provided, + the exchange will assume some (currently unknown) default values. + + Historical rate data may be incomplete. No data is published for + intervals where there are no ticks. + + **Caution**: Historical rates should not be polled frequently. + If you need real-time information, use the trade and book + endpoints along with the websocket feed. + + The maximum number of data points for a single request is 200 + candles. If your selection of start/end time and granularity + will result in more than 200 data points, your request will be + rejected. If you wish to retrieve fine granularity data over a + larger time range, you will need to make multiple requests with + new start/end ranges. + + Args: + product_id (str): Product + start (Optional[str]): Start time in ISO 8601 + end (Optional[str]): End time in ISO 8601 + granularity (Optional[str]): Desired time slice in seconds + + Returns: + list: Historic candle data. Example:: + [ + [ time, low, high, open, close, volume ], + [ 1415398768, 0.32, 4.2, 0.35, 4.2, 12.3 ], + ... + ] + + """ + params = {} + if start is not None: + params['start'] = start + if end is not None: + params['end'] = end + if granularity is not None: + params['granularity'] = granularity + r = requests.get(self.url + '/products/{}/candles' + .format(product_id), params=params) # r.raise_for_status() return r.json() - def get_product_24hr_stats(self, json=None, product=''): - if type(json) is dict: - if "product" in json: - product = json["product"] - r = requests.get(self.url + '/products/{}/stats'.format(product or self.product_id)) + def get_product_24hr_stats(self, product_id): + """Get 24 hr stats for the product. + + Args: + product_id (str): Product + + Returns: + dict: 24 hour stats. Volume is in base currency units. + Open, high, low are in quote currency units. Example:: + { + "open": "34.19000000", + "high": "95.70000000", + "low": "7.06000000", + "volume": "2.41000000" + } + + """ + r = requests.get(self.url + '/products/{}/stats'.format(product_id)) # r.raise_for_status() return r.json() def get_currencies(self): + """List known currencies. + + Returns: + list: List of currencies. Example:: + [{ + "id": "BTC", + "name": "Bitcoin", + "min_size": "0.00000001" + }, { + "id": "USD", + "name": "United States Dollar", + "min_size": "0.01000000" + }] + + """ r = requests.get(self.url + '/currencies') # r.raise_for_status() return r.json() def get_time(self): + """Get the API server time. + + Returns: + dict: Server time in ISO and epoch format (decimal seconds + since Unix epoch). Example:: + { + "iso": "2015-01-07T23:47:25.201Z", + "epoch": 1420674445.201 + } + + """ r = requests.get(self.url + '/time') # r.raise_for_status() return r.json() diff --git a/setup.py b/setup.py index 1d028f5b..3372d753 100644 --- a/setup.py +++ b/setup.py @@ -9,6 +9,10 @@ 'websocket-client==0.40.0', ] +tests_require = [ + 'pytest', + ] + setup( name='gdax', version='0.3.1', @@ -18,6 +22,7 @@ url='https://github.com/danpaquin/GDAX-Python', packages=find_packages(), install_requires=install_requires, + tests_require=tests_require, description='The unofficial Python client for the GDAX API', download_url='https://github.com/danpaquin/GDAX-Python/archive/master.zip', keywords=['gdax', 'gdax-api', 'orderbook', 'trade', 'bitcoin', 'ethereum', 'BTC', 'ETH', 'client', 'api', 'wrapper', 'exchange', 'crypto', 'currency', 'trading', 'trading-api', 'coinbase'], diff --git a/tests/test_public_client.py b/tests/test_public_client.py new file mode 100644 index 00000000..fd4850f3 --- /dev/null +++ b/tests/test_public_client.py @@ -0,0 +1,52 @@ +import pytest +import gdax + + +@pytest.fixture(scope='module') +def client(): + return gdax.PublicClient() + + +@pytest.mark.usefixtures('client') +class TestPublicClient(object): + def test_get_products(self, client): + r = client.get_products() + assert type(r) is list + + def test_get_product_order_book(self, client): + r = client.get_product_order_book() + assert type(r) is dict + r = client.get_product_order_book(level=2) + assert type(r) is dict + assert 'asks' in r + assert 'bids' in r + + def test_get_product_ticker(self, client): + r = client.get_product_ticker() + assert type(r) is dict + assert 'ask' in r + assert 'trade_id' in r + + def test_get_product_trades(self, client): + r = client.get_product_trades() + assert type(r) is list + assert 'trade_id' in r[0] + + def test_get_historic_rates(self, client): + r = client.get_product_historic_rates() + assert type(r) is list + + def test_get_product_24hr_stats(self, client): + r = client.get_product_24hr_stats() + assert type(r) is dict + assert 'volume_30day' in r + + def test_get_currencies(self, client): + r = client.get_currencies() + assert type(r) is list + assert 'name' in r[0] + + def test_get_time(self, client): + r = client.get_time() + assert type(r) is dict + assert 'iso' in r From ccf14803de751db7fc058832c63eac0e8ee41d38 Mon Sep 17 00:00:00 2001 From: Marco Montagna Date: Wed, 14 Jun 2017 22:16:56 -0700 Subject: [PATCH 037/174] Fix variable name in README. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3c44787f..400a33ed 100644 --- a/README.md +++ b/README.md @@ -255,7 +255,7 @@ class myWebsocketClient(gdax.WebsocketClient): self.message_count = 0 print("Lets count the messages!") def on_message(self, msg): - self.MessageCount += 1 + self.message_count += 1 if 'price' in msg and 'type' in msg: print ("Message type:", msg["type"], "\t@ {}.3f".format(float(msg["price"]))) def on_close(self): @@ -265,7 +265,7 @@ wsClient = myWebsocketClient() wsClient.start() print(wsClient.url, wsClient.products) while (wsClient.message_count < 500): - print ("\nMessageCount =", "{} \n".format(wsClient.message_count)) + print ("\nmessage_count =", "{} \n".format(wsClient.message_count)) time.sleep(1) wsClient.close() ``` From f0588784c1c8d2fc7b62cf91180c2739baa4c409 Mon Sep 17 00:00:00 2001 From: Bryan Kaplan <#@bryankaplan.com> Date: Sat, 17 Jun 2017 11:45:03 -0700 Subject: [PATCH 038/174] Use explicit classes when calling super() In python3, we can just say `super().__init__()`. For python2 compatibility, we need to provide two parameters to `super`: the inheriting class, and the bound object. In 2655428, we introduced calls to `super` specifying `self.__class__` for the first parameter. This is a problem (at least in python3) because that results in the inheriting class itself used for binding. Thus when we call `__init__`, it is not the `super`'s `__init__` that is called. This change-set fixes that problem by explicitly specifying the first parameter of `super` calls. This works in both python3 and python2. --- gdax/authenticated_client.py | 2 +- gdax/order_book.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index 28892437..20f3804f 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -16,7 +16,7 @@ class AuthenticatedClient(PublicClient): def __init__(self, key, b64secret, passphrase, api_url="https://api.gdax.com", product_id="BTC-USD"): - super(self.__class__, self).__init__(api_url, product_id) + super(AuthenticatedClient, self).__init__(api_url, product_id) self.auth = GdaxAuth(key, b64secret, passphrase) def get_account(self, account_id): diff --git a/gdax/order_book.py b/gdax/order_book.py index b0ef7ec8..aea30795 100644 --- a/gdax/order_book.py +++ b/gdax/order_book.py @@ -14,7 +14,7 @@ class OrderBook(WebsocketClient): def __init__(self, product_id='BTC-USD'): - super(self.__class__, self).__init__(products=product_id) + super(OrderBook, self).__init__(products=product_id) self._asks = RBTree() self._bids = RBTree() self._client = PublicClient(product_id=product_id) From c36a7ef8c414af618f9b5492ab5cfb101b5de8cf Mon Sep 17 00:00:00 2001 From: acontry Date: Thu, 15 Jun 2017 00:37:59 -0700 Subject: [PATCH 039/174] Clean up AuthenticatedClient. --- gdax/authenticated_client.py | 497 +++++++++++++++-------------- tests/test_authenticated_client.py | 36 +++ 2 files changed, 293 insertions(+), 240 deletions(-) create mode 100644 tests/test_authenticated_client.py diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index 28892437..30e545c3 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -15,273 +15,289 @@ class AuthenticatedClient(PublicClient): - def __init__(self, key, b64secret, passphrase, api_url="https://api.gdax.com", product_id="BTC-USD"): - super(self.__class__, self).__init__(api_url, product_id) + def __init__(self, key, b64secret, passphrase, + api_url="https://api.gdax.com"): + super(self.__class__, self).__init__(api_url) self.auth = GdaxAuth(key, b64secret, passphrase) + self.session = requests.Session() def get_account(self, account_id): - r = requests.get(self.url + '/accounts/' + account_id, auth=self.auth) - # r.raise_for_status() - return r.json() + return self._send_message('get', '/accounts/' + account_id) def get_accounts(self): return self.get_account('') - def get_account_history(self, account_id): - result = [] - r = requests.get(self.url + '/accounts/{}/ledger'.format(account_id), auth=self.auth) - # r.raise_for_status() - result.append(r.json()) - if "cb-after" in r.headers: - self.history_pagination(account_id, result, r.headers["cb-after"]) - return result - - def history_pagination(self, account_id, result, after): - r = requests.get(self.url + '/accounts/{}/ledger?after={}'.format(account_id, str(after)), auth=self.auth) - # r.raise_for_status() - if r.json(): - result.append(r.json()) - if "cb-after" in r.headers: - self.history_pagination(account_id, result, r.headers["cb-after"]) - return result - - def get_account_holds(self, account_id): - result = [] - r = requests.get(self.url + '/accounts/{}/holds'.format(account_id), auth=self.auth) - # r.raise_for_status() - result.append(r.json()) - if "cb-after" in r.headers: - self.holds_pagination(account_id, result, r.headers["cb-after"]) - return result - - def holds_pagination(self, account_id, result, after): - r = requests.get(self.url + '/accounts/{}/holds?after={}'.format(account_id, str(after)), auth=self.auth) - # r.raise_for_status() - if r.json(): - result.append(r.json()) - if "cb-after" in r.headers: - self.holds_pagination(account_id, result, r.headers["cb-after"]) - return result - - def buy(self, **kwargs): - kwargs["side"] = "buy" - if "product_id" not in kwargs: - kwargs["product_id"] = self.product_id - r = requests.post(self.url + '/orders', - data=json.dumps(kwargs), - auth=self.auth) - return r.json() - - def sell(self, **kwargs): - kwargs["side"] = "sell" - r = requests.post(self.url + '/orders', - data=json.dumps(kwargs), - auth=self.auth) - return r.json() + def get_account_history(self, account_id, **kwargs): + endpoint = '/accounts/{}/ledger'.format(account_id) + return self._send_message('get', endpoint, params=kwargs)[0] + + def get_account_holds(self, account_id, **kwargs): + endpoint = '/accounts/{}/holds'.format(account_id) + return self._send_message('get', endpoint, params=kwargs)[0] + + def place_order(self, product_id, side, order_type, **kwargs): + # Margin parameter checks + if kwargs.get('overdraft_enabled') is not None and \ + kwargs.get('funding_amount') is not None: + raise ValueError('Margin funding must be specified through use of ' + 'overdraft or by setting a funding amount, but not' + ' both') + + # Limit order checks + if order_type == 'limit': + if kwargs.get('cancel_after') is not None and \ + kwargs.get('tif') != 'GTT': + raise ValueError('May only specify a cancel period when time ' + 'in_force is `GTT`') + if kwargs.get('post_only') is not None and kwargs.get('tif') in \ + ['IOC', 'FOK']: + raise ValueError('post_only is invalid when time in force is ' + '`IOC` or `FOK`') + + # Market and stop order checks + if order_type == 'market' or order_type == 'stop': + if not (kwargs.get('size') is None) ^ (kwargs.get('funds') is None): + raise ValueError('Either `size` or `funds` must be specified ' + 'for market/stop orders (but not both).') + + # Build params dict + params = {'product_id': product_id, + 'side': side, + 'type': order_type + } + params.update(kwargs) + return self._send_message('post', '/orders', data=json.dumps(params)) + + def place_limit_order(self, product_id, side, price, size, + client_oid=None, + stp=None, + tif=None, + cancel_after=None, + post_only=None, + overdraft_enabled=None, + funding_amount=None): + params = {'product_id': product_id, + 'side': side, + 'order_type': 'limit', + 'price': price, + 'size': size, + 'client_oid': client_oid, + 'stp': stp, + 'tif': tif, + 'cancel_after': cancel_after, + 'post_only': post_only, + 'overdraft_enabled': overdraft_enabled, + 'funding_amount': funding_amount} + params = dict((k, v) for k, v in params.items() if v is not None) + + return self.place_order(**params) + + def place_market_order(self, product_id, side, size, funds, + client_oid=None, + stp=None, + overdraft_enabled=None, + funding_amount=None): + params = {'product_id': product_id, + 'side': side, + 'order_type': 'market', + 'size': size, + 'funds': funds, + 'client_oid': client_oid, + 'stp': stp, + 'overdraft_enabled': overdraft_enabled, + 'funding_amount': funding_amount} + params = dict((k, v) for k, v in params.items() if v is not None) + + return self.place_order(**params) + + def place_stop_order(self, product_id, side, price, size, funds, + client_oid=None, + stp=None, + overdraft_enabled=None, + funding_amount=None): + params = {'product_id': product_id, + 'side': side, + 'price': price, + 'order_type': 'stop', + 'size': size, + 'funds': funds, + 'client_oid': client_oid, + 'stp': stp, + 'overdraft_enabled': overdraft_enabled, + 'funding_amount': funding_amount} + params = dict((k, v) for k, v in params.items() if v is not None) + + return self.place_order(**params) def cancel_order(self, order_id): - r = requests.delete(self.url + '/orders/' + order_id, auth=self.auth) - # r.raise_for_status() - return r.json() - - def cancel_all(self, data=None, product=''): - if type(data) is dict: - if "product" in data: - product = data["product"] - r = requests.delete(self.url + '/orders/', - data=json.dumps({'product_id': product or self.product_id}), auth=self.auth) - # r.raise_for_status() - return r.json() + return self._send_message('delete', '/orders/' + order_id) + + def cancel_all(self, product_id=None): + if product_id is not None: + params = {'product_id': product_id} + data = json.dumps(params) + else: + data = None + return self._send_message('delete', '/orders', data=data) def get_order(self, order_id): - r = requests.get(self.url + '/orders/' + order_id, auth=self.auth) - # r.raise_for_status() - return r.json() - - def get_orders(self): - result = [] - r = requests.get(self.url + '/orders/', auth=self.auth) - # r.raise_for_status() - result.append(r.json()) - if 'cb-after' in r.headers: - self.paginate_orders(result, r.headers['cb-after']) - return result - - def paginate_orders(self, result, after): - r = requests.get(self.url + '/orders?after={}'.format(str(after))) - # r.raise_for_status() - if r.json(): - result.append(r.json()) - if 'cb-after' in r.headers: - self.paginate_orders(result, r.headers['cb-after']) - return result - - def get_fills(self, order_id='', product_id='', before='', after='', limit=''): - result = [] - url = self.url + '/fills?' - if order_id: - url += "order_id={}&".format(str(order_id)) + return self._send_message('get', '/orders/' + order_id) + + def get_orders(self, **kwargs): + return self._send_message('get', '/orders', params=kwargs)[0] + + def get_fills(self, product_id=None, order_id=None, **kwargs): + params = {} if product_id: - url += "product_id={}&".format(product_id or self.product_id) - if before: - url += "before={}&".format(str(before)) - if after: - url += "after={}&".format(str(after)) - if limit: - url += "limit={}&".format(str(limit)) - r = requests.get(url, auth=self.auth) - # r.raise_for_status() - result.append(r.json()) - if 'cb-after' in r.headers and limit is not len(r.json()): - return self.paginate_fills(result, r.headers['cb-after'], order_id=order_id, product_id=product_id) - return result - - def paginate_fills(self, result, after, order_id='', product_id=''): - url = self.url + '/fills?after={}&'.format(str(after)) + params['product_id'] = product_id if order_id: - url += "order_id={}&".format(str(order_id)) - if product_id: - url += "product_id={}&".format(product_id or self.product_id) - r = requests.get(url, auth=self.auth) - # r.raise_for_status() - if r.json(): - result.append(r.json()) - if 'cb-after' in r.headers: - return self.paginate_fills(result, r.headers['cb-after'], order_id=order_id, product_id=product_id) - return result - - def get_fundings(self, result='', status='', after=''): - if not result: - result = [] - url = self.url + '/funding?' - if status: - url += "status={}&".format(str(status)) - if after: - url += 'after={}&'.format(str(after)) - r = requests.get(url, auth=self.auth) - # r.raise_for_status() - result.append(r.json()) - if 'cb-after' in r.headers: - return self.get_fundings(result, status=status, after=r.headers['cb-after']) - return result - - def repay_funding(self, amount='', currency=''): - payload = { - "amount": amount, - "currency": currency # example: USD + params['order_id'] = order_id + params.update(kwargs) + + # Return `after` param so client can access more recent fills on next + # call of get_fills if desired. + message, r = self._send_message('get', '/fills', params=params) + return r.headers['cb-after'], message + + def get_fundings(self, status=None, **kwargs): + params = {} + if status is not None: + params['status'] = status + params.update(kwargs) + return self._send_message('get', '/funding', params=params)[0] + + def repay_funding(self, amount, currency): + params = { + 'amount': amount, + 'currency': currency # example: USD + } + return self._send_message('post', '/funding/repay', + data=json.dumps(params)) + + def margin_transfer(self, margin_profile_id, transfer_type, currency, + amount): + params = { + 'margin_profile_id': margin_profile_id, + 'type': transfer_type, + 'currency': currency, # example: USD + 'amount': amount } - r = requests.post(self.url + "/funding/repay", data=json.dumps(payload), auth=self.auth) - # r.raise_for_status() - return r.json() - - def margin_transfer(self, margin_profile_id="", transfer_type="", currency="", amount=""): - payload = { - "margin_profile_id": margin_profile_id, - "type": transfer_type, - "currency": currency, # example: USD - "amount": amount - } - r = requests.post(self.url + "/profiles/margin-transfer", data=json.dumps(payload), auth=self.auth) - # r.raise_for_status() - return r.json() + return self._send_message('post', '/profiles/margin-transfer', + data=json.dumps(params)) def get_position(self): - r = requests.get(self.url + "/position", auth=self.auth) - # r.raise_for_status() - return r.json() - - def close_position(self, repay_only=""): - payload = { - "repay_only": repay_only or False + return self._send_message('get', '/position')[0] + + def close_position(self, repay_only): + params = {'repay_only': repay_only} + return self._send_message('post', '/position/close', + data=json.dumps(params))[0] + + def deposit(self, amount, currency, payment_method_id): + params = { + 'amount': amount, + 'currency': currency, + 'payment_method_id': payment_method_id } - r = requests.post(self.url + "/position/close", data=json.dumps(payload), auth=self.auth) - # r.raise_for_status() - return r.json() - - def deposit(self, amount="", currency="", payment_method_id=""): - payload = { - "amount": amount, - "currency": currency, - "payment_method_id": payment_method_id + return self._send_message('post', '/deposits/payment-method', + data=json.dumps(params))[0] + + def coinbase_deposit(self, amount, currency, coinbase_account_id): + params = { + 'amount': amount, + 'currency': currency, + 'coinbase_account_id': coinbase_account_id } - r = requests.post(self.url + "/deposits/payment-method", data=json.dumps(payload), auth=self.auth) - # r.raise_for_status() - return r.json() - - def coinbase_deposit(self, amount="", currency="", coinbase_account_id=""): - payload = { - "amount": amount, - "currency": currency, - "coinbase_account_id": coinbase_account_id + return self._send_message('post', '/deposits/coinbase-account', + data=json.dumps(params))[0] + + def withdraw(self, amount, currency, payment_method_id): + params = { + 'amount': amount, + 'currency': currency, + 'payment_method_id': payment_method_id } - r = requests.post(self.url + "/deposits/coinbase-account", data=json.dumps(payload), auth=self.auth) - # r.raise_for_status() - return r.json() - - def withdraw(self, amount="", currency="", payment_method_id=""): - payload = { - "amount": amount, - "currency": currency, - "payment_method_id": payment_method_id + return self._send_message('post', '/withdrawals/payment-method', + data=json.dumps(params))[0] + + def coinbase_withdraw(self, amount, currency, coinbase_account_id): + params = { + 'amount': amount, + 'currency': currency, + 'coinbase_account_id': coinbase_account_id } - r = requests.post(self.url + "/withdrawals/payment-method", data=json.dumps(payload), auth=self.auth) - # r.raise_for_status() - return r.json() - - def coinbase_withdraw(self, amount="", currency="", coinbase_account_id=""): - payload = { - "amount": amount, - "currency": currency, - "coinbase_account_id": coinbase_account_id + return self._send_message('post', '/withdrawals/coinbase', + data=json.dumps(params))[0] + + def crypto_withdraw(self, amount, currency, crypto_address): + params = { + 'amount': amount, + 'currency': currency, + 'crypto_address': crypto_address } - r = requests.post(self.url + "/withdrawals/coinbase", data=json.dumps(payload), auth=self.auth) - # r.raise_for_status() - return r.json() - - def crypto_withdraw(self, amount="", currency="", crypto_address=""): - payload = { - "amount": amount, - "currency": currency, - "crypto_address": crypto_address - } - r = requests.post(self.url + "/withdrawals/crypto", data=json.dumps(payload), auth=self.auth) - # r.raise_for_status() - return r.json() + return self._send_message('post', '/withdrawals/crypto', + data=json.dumps(params))[0] def get_payment_methods(self): - r = requests.get(self.url + "/payment-methods", auth=self.auth) - # r.raise_for_status() - return r.json() + return self._send_message('get', '/payment-methods')[0] def get_coinbase_accounts(self): - r = requests.get(self.url + "/coinbase-accounts", auth=self.auth) - # r.raise_for_status() - return r.json() - - def create_report(self, report_type="", start_date="", end_date="", product_id="", account_id="", report_format="", - email=""): - payload = { - "type": report_type, - "start_date": start_date, - "end_date": end_date, - "product_id": product_id, - "account_id": account_id, - "format": report_format, - "email": email + return self._send_message('get', '/coinbase-accounts')[0] + + def create_report(self, report_type, start_date, end_date, product_id=None, + account_id=None, report_format='pdf', email=None): + params = { + 'type': report_type, + 'start_date': start_date, + 'end_date': end_date, + 'format': report_format, } - r = requests.post(self.url + "/reports", data=json.dumps(payload), auth=self.auth) - # r.raise_for_status() - return r.json() + if product_id is not None: + params['product_id'] = product_id + if account_id is not None: + params['account_id'] = account_id + if email is not None: + params['email'] = email + + return self._send_message('post', '/reports', + data=json.dumps(params))[0] - def get_report(self, report_id=""): - r = requests.get(self.url + "/reports/" + report_id, auth=self.auth) - # r.raise_for_status() - return r.json() + def get_report(self, report_id): + return self._send_message('get', '/reports/' + report_id)[0] def get_trailing_volume(self): - r = requests.get(self.url + "/users/self/trailing-volume", auth=self.auth) - # r.raise_for_status() - return r.json() + return self._send_message('get', '/users/self/trailing-volume')[0] + + def _send_message(self, method, endpoint, params=None, data=None): + """Get a paginated response by making multiple http requests. + + Args: + method (str): HTTP method (get, post, delete, etc.) + endpoint (str): Endpoint (to be added to base URL) + params (Optional[dict]): HTTP request parameters + data (Optional[str]): JSON-encoded string payload for POST + + Returns: + list: Merged responses from paginated requests + requests.models.Response: Response object from last HTTP + response + + """ + if params is None: + params = {} + response_data = [] + url = self.url + endpoint + r = self.session.request(method, url, params=params, data=data, + auth=self.auth) + if r.json(): + response_data = r.json() + if method == 'get': + while 'cb-after' in r.headers: + params['after'] = r.headers['cb-after'] + r = self.session.get(url, params=params, auth=self.auth) + if r.json(): + response_data += r.json() + return response_data, r class GdaxAuth(AuthBase): @@ -293,13 +309,14 @@ def __init__(self, api_key, secret_key, passphrase): def __call__(self, request): timestamp = str(time.time()) - message = timestamp + request.method + request.path_url + (request.body or '') + message = timestamp + request.method + request.path_url + \ + (request.body or '') message = message.encode('ascii') hmac_key = base64.b64decode(self.secret_key) signature = hmac.new(hmac_key, message, hashlib.sha256) signature_b64 = base64.b64encode(signature.digest()) request.headers.update({ - 'Content-Type': 'Application/JSON', + 'Content-Type': 'Application/json', 'CB-ACCESS-SIGN': signature_b64, 'CB-ACCESS-TIMESTAMP': timestamp, 'CB-ACCESS-KEY': self.api_key, diff --git a/tests/test_authenticated_client.py b/tests/test_authenticated_client.py new file mode 100644 index 00000000..b9ebb1b7 --- /dev/null +++ b/tests/test_authenticated_client.py @@ -0,0 +1,36 @@ +import pytest +import gdax + + +@pytest.fixture(scope='module') +def dc(): + """Dummy client for testing.""" + return gdax.AuthenticatedClient('test', 'test', 'test') + + +@pytest.mark.usefixtures('dc') +class TestAuthenticatedClient(object): + def test_place_order_input_1(self, dc): + with pytest.raises(ValueError): + r = dc.place_order('BTC-USD', 'buy', 'market', + overdraft_enabled='true', funding_amount=10) + + def test_place_order_input_2(self, dc): + with pytest.raises(ValueError): + r = dc.place_order('BTC-USD', 'buy', 'limit', + cancel_after='123', tif='ABC') + + def test_place_order_input_3(self, dc): + with pytest.raises(ValueError): + r = dc.place_order('BTC-USD', 'buy', 'limit', + post_only='true', tif='FOK') + + def test_place_order_input_4(self, dc): + with pytest.raises(ValueError): + r = dc.place_order('BTC-USD', 'buy', 'market', + size=None, funds=None) + + def test_place_order_input_5(self, dc): + with pytest.raises(ValueError): + r = dc.place_order('BTC-USD', 'buy', 'market', + size=1, funds=1) From 5283ca478cb07c4c3371c22d82d8052e3885ad0c Mon Sep 17 00:00:00 2001 From: Ted McCormack Date: Sat, 17 Jun 2017 13:04:46 -0700 Subject: [PATCH 040/174] Minor cleanups for thread handling --- GDAX/OrderBook.py | 4 ++++ GDAX/WebsocketClient.py | 13 ++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/GDAX/OrderBook.py b/GDAX/OrderBook.py index 5520322e..cb07d5f2 100644 --- a/GDAX/OrderBook.py +++ b/GDAX/OrderBook.py @@ -80,6 +80,10 @@ def onMessage(self, message): # asks = self.get_asks(ask) # ask_depth = sum([a['size'] for a in asks]) # print('bid: %f @ %f - ask: %f @ %f' % (bid_depth, bid, ask_depth, ask)) + def onError(self, e): + self._sequence = -1 + self.close() + self.start() def add(self, order): order = { diff --git a/GDAX/WebsocketClient.py b/GDAX/WebsocketClient.py index dc47a7e6..7f071e5c 100644 --- a/GDAX/WebsocketClient.py +++ b/GDAX/WebsocketClient.py @@ -29,11 +29,11 @@ def _go(): self._listen() self.onOpen() - self.ws = create_connection(self.url) self.thread = Thread(target=_go) self.thread.start() def _connect(self): + self.ws = create_connection(self.url) if self.products is None: self.products = ["BTC-USD"] elif not isinstance(self.products, list): @@ -54,10 +54,8 @@ def _listen(self): try: msg = json.loads(self.ws.recv()) except Exception as e: - traceback.print_exc() + #traceback.print_exc() self.onError(e) - self.close() - self.start() else: self.onMessage(msg) @@ -67,9 +65,10 @@ def close(self): self.ws.send(json.dumps({"type": "heartbeat", "on": False})) self.onClose() self.stop = True - #self.thread = None - self.thread.join() - self.ws.close() + try: + self.ws.close() + except WebSocketConnectionClosedException as e: + pass def onOpen(self): print("-- Subscribed! --\n") From 508b3bf534a1f180d58d0a43956138aad1d3ed6f Mon Sep 17 00:00:00 2001 From: Ted McCormack Date: Sat, 17 Jun 2017 13:35:53 -0700 Subject: [PATCH 041/174] Allow alternate url for order book, keep log_to, ws connect in correct place --- gdax/order_book.py | 9 ++++----- gdax/websocket_client.py | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/gdax/order_book.py b/gdax/order_book.py index 05af46c6..11fc6fb6 100644 --- a/gdax/order_book.py +++ b/gdax/order_book.py @@ -14,17 +14,16 @@ class OrderBook(WebsocketClient): - def __init__(self, product_id='BTC-USD'): - super(self.__class__, self).__init__(products=product_id) + def __init__(self, url="wss://ws-feed.gdax.com", product_id='BTC-USD', log_to=None): + super(self.__class__, self).__init__(url=url, products=product_id) self._asks = RBTree() self._bids = RBTree() self._client = PublicClient(product_id=product_id) self._sequence = -1 - if log_to: - assert hasattr(log_to, 'write') self._log_to = log_to + if self._log_to: + assert hasattr(self._log_to, 'write') self._current_ticker = None - self.__live = live def on_message(self, message): if self._log_to: diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index 21c1b11b..bece0a81 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -30,8 +30,6 @@ def _go(): self.thread.start() def _connect(self): - self.ws = create_connection(self.url) - if self.products is None: self.products = ["BTC-USD"] elif not isinstance(self.products, list): @@ -40,6 +38,8 @@ def _connect(self): if self.url[-1] == "/": self.url = self.url[:-1] + self.ws = create_connection(self.url) + self.stop = False sub_params = {'type': 'subscribe', 'product_ids': self.products} self.ws.send(json.dumps(sub_params)) From 5fc68d27b7ad45414710f00b4676090e999e6a15 Mon Sep 17 00:00:00 2001 From: Ted McCormack Date: Sat, 17 Jun 2017 16:10:34 -0700 Subject: [PATCH 042/174] Forgot import of exception --- gdax/websocket_client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index bece0a81..28633e32 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -8,7 +8,7 @@ import json from threading import Thread -from websocket import create_connection +from websocket import create_connection, WebSocketConnectionClosedException class WebsocketClient(object): @@ -63,7 +63,8 @@ def close(self): self.on_close() self.stop = True try: - self.ws.close() + if self.ws: + self.ws.close() except WebSocketConnectionClosedException as e: pass From b6015e45b02fc5396c58cc709994a2f9fc643d25 Mon Sep 17 00:00:00 2001 From: acontry Date: Sat, 17 Jun 2017 22:24:49 -0700 Subject: [PATCH 043/174] Clean up PublicClient. Add docstrings + tests. Drop the mixed keyword/dict interface in favor of keywords only. Add docstrings based on Google format. --- gdax/public_client.py | 247 ++++++++++++++++++++++++++++++------ setup.py | 5 + tests/test_public_client.py | 52 ++++++++ 3 files changed, 268 insertions(+), 36 deletions(-) create mode 100644 tests/test_public_client.py diff --git a/gdax/public_client.py b/gdax/public_client.py index 5742b9d8..ecbc7880 100644 --- a/gdax/public_client.py +++ b/gdax/public_client.py @@ -8,69 +8,244 @@ class PublicClient(object): - def __init__(self, api_url="https://api.gdax.com", product_id="BTC-USD"): - self.url = api_url.rstrip("/") - self.product_id = product_id + """GDAX public client API. + + All requests default to the `product_id` specified at object + creation if not otherwise specified. + + Attributes: + url (Optional[str]): API URL. Defaults to GDAX API. + + """ + + def __init__(self, api_url='https://api.gdax.com'): + """Create GDAX API public client. + + Args: + api_url (Optional[str]): API URL. Defaults to GDAX API. + + """ + self.url = api_url.rstrip('/') def get_products(self): + """Get a list of available currency pairs for trading. + + Returns: + list: Info about all currency pairs. Example:: + [ + { + "id": "BTC-USD", + "display_name": "BTC/USD", + "base_currency": "BTC", + "quote_currency": "USD", + "base_min_size": "0.01", + "base_max_size": "10000.00", + "quote_increment": "0.01" + } + ] + + """ r = requests.get(self.url + '/products') # r.raise_for_status() return r.json() - def get_product_order_book(self, json=None, level=2, product=''): - if type(json) is dict: - if "product" in json: - product = json["product"] - if "level" in json: - level = json['level'] - r = requests.get(self.url + '/products/{}/book?level={}'.format(product or self.product_id, str(level))) + def get_product_order_book(self, product_id, level=1): + """Get a list of open orders for a product. + + The amount of detail shown can be customized with the `level` + parameter: + * 1: Only the best bid and ask + * 2: Top 50 bids and asks (aggregated) + * 3: Full order book (non aggregated) + + Level 1 and Level 2 are recommended for polling. For the most + up-to-date data, consider using the websocket stream. + + **Caution**: Level 3 is only recommended for users wishing to + maintain a full real-time order book using the websocket + stream. Abuse of Level 3 via polling will cause your access to + be limited or blocked. + + Args: + product_id (str): Product + level (Optional[int]): Order book level (1, 2, or 3). + Default is 1. + + Returns: + dict: Order book. Example for level 1:: + { + "sequence": "3", + "bids": [ + [ price, size, num-orders ], + ], + "asks": [ + [ price, size, num-orders ], + ] + } + + """ + params = {'level': level} + r = requests.get(self.url + '/products/{}/book' + .format(product_id), params=params) # r.raise_for_status() return r.json() - def get_product_ticker(self, json=None, product=''): - if type(json) is dict: - if "product" in json: - product = json["product"] - r = requests.get(self.url + '/products/{}/ticker'.format(product or self.product_id)) + def get_product_ticker(self, product_id): + """Snapshot about the last trade (tick), best bid/ask and 24h volume. + + **Caution**: Polling is discouraged in favor of connecting via + the websocket stream and listening for match messages. + + Args: + product_id (str): Product + + Returns: + dict: Ticker info. Example:: + { + "trade_id": 4729088, + "price": "333.99", + "size": "0.193", + "bid": "333.98", + "ask": "333.99", + "volume": "5957.11914015", + "time": "2015-11-14T20:46:03.511254Z" + } + + """ + r = requests.get(self.url + '/products/{}/ticker' + .format(product_id)) # r.raise_for_status() return r.json() - def get_product_trades(self, json=None, product=''): - if type(json) is dict: - if "product" in json: - product = json["product"] - r = requests.get(self.url + '/products/{}/trades'.format(product or self.product_id)) + def get_product_trades(self, product_id): + """List the latest trades for a product. + + Args: + product_id (str): Product + + Returns: + list: Latest trades. Example:: + [{ + "time": "2014-11-07T22:19:28.578544Z", + "trade_id": 74, + "price": "10.00000000", + "size": "0.01000000", + "side": "buy" + }, { + "time": "2014-11-07T01:08:43.642366Z", + "trade_id": 73, + "price": "100.00000000", + "size": "0.01000000", + "side": "sell" + }] + + """ + r = requests.get(self.url + '/products/{}/trades'.format(product_id)) # r.raise_for_status() return r.json() - def get_product_historic_rates(self, json=None, product='', start='', end='', granularity=''): - payload = {} - if type(json) is dict: - if "product" in json: - product = json["product"] - payload = json - else: - payload["start"] = start - payload["end"] = end - payload["granularity"] = granularity - r = requests.get(self.url + '/products/{}/candles'.format(product or self.product_id), params=payload) + def get_product_historic_rates(self, product_id, start=None, end=None, + granularity=None): + """Historic rates for a product. + + Rates are returned in grouped buckets based on requested + `granularity`. If start, end, and granularity aren't provided, + the exchange will assume some (currently unknown) default values. + + Historical rate data may be incomplete. No data is published for + intervals where there are no ticks. + + **Caution**: Historical rates should not be polled frequently. + If you need real-time information, use the trade and book + endpoints along with the websocket feed. + + The maximum number of data points for a single request is 200 + candles. If your selection of start/end time and granularity + will result in more than 200 data points, your request will be + rejected. If you wish to retrieve fine granularity data over a + larger time range, you will need to make multiple requests with + new start/end ranges. + + Args: + product_id (str): Product + start (Optional[str]): Start time in ISO 8601 + end (Optional[str]): End time in ISO 8601 + granularity (Optional[str]): Desired time slice in seconds + + Returns: + list: Historic candle data. Example:: + [ + [ time, low, high, open, close, volume ], + [ 1415398768, 0.32, 4.2, 0.35, 4.2, 12.3 ], + ... + ] + + """ + params = {} + if start is not None: + params['start'] = start + if end is not None: + params['end'] = end + if granularity is not None: + params['granularity'] = granularity + r = requests.get(self.url + '/products/{}/candles' + .format(product_id), params=params) # r.raise_for_status() return r.json() - def get_product_24hr_stats(self, json=None, product=''): - if type(json) is dict: - if "product" in json: - product = json["product"] - r = requests.get(self.url + '/products/{}/stats'.format(product or self.product_id)) + def get_product_24hr_stats(self, product_id): + """Get 24 hr stats for the product. + + Args: + product_id (str): Product + + Returns: + dict: 24 hour stats. Volume is in base currency units. + Open, high, low are in quote currency units. Example:: + { + "open": "34.19000000", + "high": "95.70000000", + "low": "7.06000000", + "volume": "2.41000000" + } + + """ + r = requests.get(self.url + '/products/{}/stats'.format(product_id)) # r.raise_for_status() return r.json() def get_currencies(self): + """List known currencies. + + Returns: + list: List of currencies. Example:: + [{ + "id": "BTC", + "name": "Bitcoin", + "min_size": "0.00000001" + }, { + "id": "USD", + "name": "United States Dollar", + "min_size": "0.01000000" + }] + + """ r = requests.get(self.url + '/currencies') # r.raise_for_status() return r.json() def get_time(self): + """Get the API server time. + + Returns: + dict: Server time in ISO and epoch format (decimal seconds + since Unix epoch). Example:: + { + "iso": "2015-01-07T23:47:25.201Z", + "epoch": 1420674445.201 + } + + """ r = requests.get(self.url + '/time') # r.raise_for_status() return r.json() diff --git a/setup.py b/setup.py index 1d028f5b..3372d753 100644 --- a/setup.py +++ b/setup.py @@ -9,6 +9,10 @@ 'websocket-client==0.40.0', ] +tests_require = [ + 'pytest', + ] + setup( name='gdax', version='0.3.1', @@ -18,6 +22,7 @@ url='https://github.com/danpaquin/GDAX-Python', packages=find_packages(), install_requires=install_requires, + tests_require=tests_require, description='The unofficial Python client for the GDAX API', download_url='https://github.com/danpaquin/GDAX-Python/archive/master.zip', keywords=['gdax', 'gdax-api', 'orderbook', 'trade', 'bitcoin', 'ethereum', 'BTC', 'ETH', 'client', 'api', 'wrapper', 'exchange', 'crypto', 'currency', 'trading', 'trading-api', 'coinbase'], diff --git a/tests/test_public_client.py b/tests/test_public_client.py new file mode 100644 index 00000000..5da77c27 --- /dev/null +++ b/tests/test_public_client.py @@ -0,0 +1,52 @@ +import pytest +import gdax + + +@pytest.fixture(scope='module') +def client(): + return gdax.PublicClient() + + +@pytest.mark.usefixtures('client') +class TestPublicClient(object): + def test_get_products(self, client): + r = client.get_products() + assert type(r) is list + + def test_get_product_order_book(self, client): + r = client.get_product_order_book('BTC-USD') + assert type(r) is dict + r = client.get_product_order_book('BTC-USD', level=2) + assert type(r) is dict + assert 'asks' in r + assert 'bids' in r + + def test_get_product_ticker(self, client): + r = client.get_product_ticker('BTC-USD') + assert type(r) is dict + assert 'ask' in r + assert 'trade_id' in r + + def test_get_product_trades(self, client): + r = client.get_product_trades('BTC-USD') + assert type(r) is list + assert 'trade_id' in r[0] + + def test_get_historic_rates(self, client): + r = client.get_product_historic_rates('BTC-USD') + assert type(r) is list + + def test_get_product_24hr_stats(self, client): + r = client.get_product_24hr_stats('BTC-USD') + assert type(r) is dict + assert 'volume_30day' in r + + def test_get_currencies(self, client): + r = client.get_currencies() + assert type(r) is list + assert 'name' in r[0] + + def test_get_time(self, client): + r = client.get_time() + assert type(r) is dict + assert 'iso' in r From c3732a1ac2949436ccab49a46c68ca3513513e9b Mon Sep 17 00:00:00 2001 From: dj Date: Sun, 18 Jun 2017 23:31:13 -0400 Subject: [PATCH 044/174] added most of the docstrings --- gdax/authenticated_client.py | 362 ++++++++++++++++++++++++++++++++++- 1 file changed, 361 insertions(+), 1 deletion(-) diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index 30e545c3..26e47824 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -15,23 +15,153 @@ class AuthenticatedClient(PublicClient): + """ Provides access to Private Endpoints on the GDAX API. + + All requests default to the live `api_url`: 'https://api.gdax.com'. + To test your application using the sandbox modify the `api_url`. + + Attributes: + url (str): The api url for this client instance to use. + auth (GdaxAuth): Custom authentication handler for each request. + session (requests.Session): Persistent HTTP connection object. + """ def __init__(self, key, b64secret, passphrase, api_url="https://api.gdax.com"): + """ Create an instance of the AuthenticatedClient class. + + Args: + key (str): Your API key. + b64secret (str): The secret key matching your API key. + passphrase (str): Passphrase chosen when setting up key. + api_url (Optional[str]): API URL. Defaults to GDAX API. + """ super(self.__class__, self).__init__(api_url) self.auth = GdaxAuth(key, b64secret, passphrase) self.session = requests.Session() def get_account(self, account_id): + """ Get information for a single account. + + Use this endpoint when you know the account_id. + + Args: + account_id (str): Account id for account you want to get. + + Returns: + dict: Account information. Example:: + { + "id": "a1b2c3d4", + "balance": "1.100", + "holds": "0.100", + "available": "1.00", + "currency": "USD" + } + """ return self._send_message('get', '/accounts/' + account_id) def get_accounts(self): + """ Get a list of trading all accounts. + + When you place an order, the funds for the order are placed on + hold. They cannot be used for other orders or withdrawn. Funds + will remain on hold until the order is filled or canceled. The + funds on hold for each account will be specified. + + Returns: + list: Info about all accounts. Example:: + [ + { + "id": "71452118-efc7-4cc4-8780-a5e22d4baa53", + "currency": "BTC", + "balance": "0.0000000000000000", + "available": "0.0000000000000000", + "hold": "0.0000000000000000", + "profile_id": "75da88c5-05bf-4f54-bc85-5c775bd68254" + }, + { + "id": "e316cb9a-0808-4fd7-8914-97829c1925de", + "currency": "USD", + "balance": "80.2301373066930000", + "available": "79.2266348066930000", + "hold": "1.0035025000000000", + "profile_id": "75da88c5-05bf-4f54-bc85-5c775bd68254" + } + ] + + * Additional info included in response for margin accounts. + """ return self.get_account('') def get_account_history(self, account_id, **kwargs): + """ List account activity. Account activity either increases or + decreases your account balance. + + Entry type indicates the reason for the account change. + * transfer: Funds moved to/from Coinbase to GDAX + * match: Funds moved as a result of a trade + * fee: Fee as a result of a trade + * rebate: Fee rebate as per our fee schedule + + If an entry is the result of a trade (match, fee), the details + field will contain additional information about the trade. + + Args: + account_id (str): Account id to get history of. + kwargs (dict): Additional HTTP request parameters. + + Returns: + list: History information for the account. Example:: + [ + { + "id": "100", + "created_at": "2014-11-07T08:19:27.028459Z", + "amount": "0.001", + "balance": "239.669", + "type": "fee", + "details": { + "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", + "trade_id": "74", + "product_id": "BTC-USD" + } + } + ] + """ endpoint = '/accounts/{}/ledger'.format(account_id) return self._send_message('get', endpoint, params=kwargs)[0] def get_account_holds(self, account_id, **kwargs): + """ Holds are placed on an account for active orders or pending + withdraw requests. + + As an order is filled, the hold amount is updated. If an order + is canceled, any remaining hold is removed. For a withdraw, once + it is completed, the hold is removed. + + The `type` field will indicate why the hold exists. The hold + type is 'order' for holds related to open orders and 'transfer' + for holds related to a withdraw. + + The `ref` field contains the id of the order or transfer which + created the hold. + + Args: + account_id (str): Account id to get holds of. + kwargs (dict): Additional HTTP request parameters. + + Returns: + list: Hold information for the provided account. Example:: + [ + { + "id": "82dcd140-c3c7-4507-8de4-2c529cd1a28f", + "account_id": "e0b3f39a-183d-453e-b754-0c13e5bab0b3", + "created_at": "2014-11-06T10:34:47.123456Z", + "updated_at": "2014-11-06T10:40:47.123456Z", + "amount": "4.23", + "type": "order", + "ref": "0a205de4-dd35-4370-a285-fe8fc375a273", + } + ] + """ endpoint = '/accounts/{}/holds'.format(account_id) return self._send_message('get', endpoint, params=kwargs)[0] @@ -64,7 +194,7 @@ def place_order(self, product_id, side, order_type, **kwargs): params = {'product_id': product_id, 'side': side, 'type': order_type - } + } params.update(kwargs) return self._send_message('post', '/orders', data=json.dumps(params)) @@ -130,9 +260,42 @@ def place_stop_order(self, product_id, side, price, size, funds, return self.place_order(**params) def cancel_order(self, order_id): + """ Cancel a previously placed order. + + If the order had no matches during its lifetime its record may + be purged. This means the order details will not be available + with get_order(order_id). If the order could not be canceled + (already filled or previously canceled, etc), then an error + response will indicate the reason in the message field. + + **Caution**: The order id is the server-assigned order id and + not the optional client_oid. + + Args: + order_id (str): The order_id of the order you want to cancel + + Returns: + list: Containing the order_id of cancelled order. Example:: + [ "c5ab5eae-76be-480e-8961-00792dc7e138" ] + """ return self._send_message('delete', '/orders/' + order_id) def cancel_all(self, product_id=None): + """ With best effort, cancel all open orders. + + Args: + product_id (Optional[str]): Only cancel ordrers for this product_id + + Returns: + list: A list of ids of the canceled orders. Example:: + [ + "144c6f8e-713f-4682-8435-5280fbe8b2b4", + "debe4907-95dc-442f-af3b-cec12f42ebda", + "cf7aceee-7b08-4227-a76c-3858144323ab", + "dfc5ae27-cadb-4c0c-beef-8994936fde8a", + "34fecfbf-de33-4273-b2c6-baf8e8948be4" + ] + """ if product_id is not None: params = {'product_id': product_id} data = json.dumps(params) @@ -141,12 +304,131 @@ def cancel_all(self, product_id=None): return self._send_message('delete', '/orders', data=data) def get_order(self, order_id): + """ Get a single order by order id. + + If the order is canceled the response may have status code 404 + if the order had no matches. + + **Caution**: Open orders may change state between the request + and the response depending on market conditions. + + Args: + order_id (str): The order to get information of. + + Returns: + dict: Containing information on order. Example:: + { + "created_at": "2017-06-18T00:27:42.920136Z", + "executed_value": "0.0000000000000000", + "fill_fees": "0.0000000000000000", + "filled_size": "0.00000000", + "id": "9456f388-67a9-4316-bad1-330c5353804f", + "post_only": true, + "price": "1.00000000", + "product_id": "BTC-USD", + "settled": false, + "side": "buy", + "size": "1.00000000", + "status": "pending", + "stp": "dc", + "time_in_force": "GTC", + "type": "limit" + } + """ return self._send_message('get', '/orders/' + order_id) def get_orders(self, **kwargs): + """ List your current open orders. + + Only open or un-settled orders are returned. As soon as an order + is no longer open and settled, it will no longer appear in the + default request. + + Orders which are no longer resting on the order book, will be + marked with the 'done' status. There is a small window between + an order being 'done' and 'settled'. An order is 'settled' when + all of the fills have settled and the remaining holds (if any) + have been removed. + + For high-volume trading it is strongly recommended that you + maintain your own list of open orders and use one of the + streaming market data feeds to keep it updated. You should poll + the open orders endpoint once when you start trading to obtain + the current state of any open orders. + + Args: + kwargs (dict): usage below + * status: Limit list of orders to these statuses. + Passing 'all' returns orders of all statuses. + ** default: ['open', 'pending', 'active'] + * product_id: Only list orders for a specific product + + Returns: + list: Containing information on orders. Example:: + [ + { + "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2", + "price": "0.10000000", + "size": "0.01000000", + "product_id": "BTC-USD", + "side": "buy", + "stp": "dc", + "type": "limit", + "time_in_force": "GTC", + "post_only": false, + "created_at": "2016-12-08T20:02:28.53864Z", + "fill_fees": "0.0000000000000000", + "filled_size": "0.00000000", + "executed_value": "0.0000000000000000", + "status": "open", + "settled": false + }, + { + ... + } + ] + """ return self._send_message('get', '/orders', params=kwargs)[0] def get_fills(self, product_id=None, order_id=None, **kwargs): + """ Get a list of recent fills. + + Fees are recorded in two stages. Immediately after the matching + engine completes a match, the fill is inserted into our + datastore. Once the fill is recorded, a settlement process will + settle the fill and credit both trading counterparties. + + The 'fee' field indicates the fees charged for this fill. + + The 'liquidity' field indicates if the fill was the result of a + liquidity provider or liquidity taker. M indicates Maker and T + indicates Taker. + + Args: + product_id (Optional[str]): Limit list to this product_id + order_id (Optional[str]): Limit list to this order_id + kwargs (dict): Additional HTTP request parameters. + + Returns: + list: Containing information on fills. Example:: + [ + { + "trade_id": 74, + "product_id": "BTC-USD", + "price": "10.00", + "size": "0.01", + "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", + "created_at": "2014-11-07T22:19:28.578544Z", + "liquidity": "T", + "fee": "0.00025", + "settled": true, + "side": "buy" + }, + { + ... + } + ] + """ params = {} if product_id: params['product_id'] = product_id @@ -160,6 +442,34 @@ def get_fills(self, product_id=None, order_id=None, **kwargs): return r.headers['cb-after'], message def get_fundings(self, status=None, **kwargs): + """ Every order placed with a margin profile that draws funding + will create a funding record. + + Args: + status (list/str): Limit funding records to these statuses. + ** Options: 'open', 'active', 'pending' + kwargs (dict): Additional HTTP request parameters. + + Returns: + list: Containing information on margin funding. Example:: + [ + { + "id": "b93d26cd-7193-4c8d-bfcc-446b2fe18f71", + "order_id": "b93d26cd-7193-4c8d-bfcc-446b2fe18f71", + "profile_id": "d881e5a6-58eb-47cd-b8e2-8d9f2e3ec6f6", + "amount": "1057.6519956381537500", + "status": "settled", + "created_at": "2017-03-17T23:46:16.663397Z", + "currency": "USD", + "repaid_amount": "1057.6519956381537500", + "default_amount": "0", + "repaid_default": false + }, + { + ... + } + ] + """ params = {} if status is not None: params['status'] = status @@ -167,6 +477,15 @@ def get_fundings(self, status=None, **kwargs): return self._send_message('get', '/funding', params=params)[0] def repay_funding(self, amount, currency): + """ Repay funding. Repays the older funding records first. + + Args: + amount (int): Amount of currency to repay + currency (str): The currency, example USD + + Returns: + ??? + """ params = { 'amount': amount, 'currency': currency # example: USD @@ -194,6 +513,25 @@ def close_position(self, repay_only): data=json.dumps(params))[0] def deposit(self, amount, currency, payment_method_id): + """ Deposit funds from a payment method. + + See AuthenticatedClient.get_payment_methods() to receive + information regarding payment methods. + + Args: + amount (int): The amount to depost. + currency (str): The type of currency. + payment_method_id (str): ID of the payment method. + + Returns: + dict: Information about the deposit. Example:: + { + "id": "593533d2-ff31-46e0-b22e-ca754147a96a", + "amount": "10.00", + "currency": "USD", + "payout_at": "2016-08-20T00:31:09Z" + } + """ params = { 'amount': amount, 'currency': currency, @@ -203,6 +541,28 @@ def deposit(self, amount, currency, payment_method_id): data=json.dumps(params))[0] def coinbase_deposit(self, amount, currency, coinbase_account_id): + """ Deposit funds from a coinbase account. + + You can move funds between your Coinbase accounts and your GDAX + trading accounts within your daily limits. Moving funds between + Coinbase and GDAX is instant and free. + + See AuthenticatedClient.get_coinbase_accounts() to receive + information regarding your coinbase_accounts. + + Args: + amount (int): The amount to depost. + currency (str): The type of currency. + coinbase_account_id (str): ID of the coinbase account. + + Returns: + dict: Information about the deposit. Example:: + { + "id": "593533d2-ff31-46e0-b22e-ca754147a96a", + "amount": "10.00", + "currency": "BTC", + } + """ params = { 'amount': amount, 'currency': currency, From 1577df80655db27578fc7cf0b0a2a12b65c76e9a Mon Sep 17 00:00:00 2001 From: acontry Date: Mon, 19 Jun 2017 01:42:35 -0700 Subject: [PATCH 045/174] Convert paginated requests to use generators --- gdax/authenticated_client.py | 181 +++++++++++++++++------------------ 1 file changed, 87 insertions(+), 94 deletions(-) diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index 26e47824..95601909 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -35,7 +35,7 @@ def __init__(self, key, b64secret, passphrase, passphrase (str): Passphrase chosen when setting up key. api_url (Optional[str]): API URL. Defaults to GDAX API. """ - super(self.__class__, self).__init__(api_url) + super(AuthenticatedClient, self).__init__(api_url) self.auth = GdaxAuth(key, b64secret, passphrase) self.session = requests.Session() @@ -79,12 +79,7 @@ def get_accounts(self): "profile_id": "75da88c5-05bf-4f54-bc85-5c775bd68254" }, { - "id": "e316cb9a-0808-4fd7-8914-97829c1925de", - "currency": "USD", - "balance": "80.2301373066930000", - "available": "79.2266348066930000", - "hold": "1.0035025000000000", - "profile_id": "75da88c5-05bf-4f54-bc85-5c775bd68254" + ... } ] @@ -123,11 +118,14 @@ def get_account_history(self, account_id, **kwargs): "trade_id": "74", "product_id": "BTC-USD" } + }, + { + ... } ] """ endpoint = '/accounts/{}/ledger'.format(account_id) - return self._send_message('get', endpoint, params=kwargs)[0] + return self._send_paginated_message(endpoint, params=kwargs) def get_account_holds(self, account_id, **kwargs): """ Holds are placed on an account for active orders or pending @@ -159,11 +157,14 @@ def get_account_holds(self, account_id, **kwargs): "amount": "4.23", "type": "order", "ref": "0a205de4-dd35-4370-a285-fe8fc375a273", + }, + { + ... } ] """ endpoint = '/accounts/{}/holds'.format(account_id) - return self._send_message('get', endpoint, params=kwargs)[0] + return self._send_paginated_message(endpoint, params=kwargs) def place_order(self, product_id, side, order_type, **kwargs): # Margin parameter checks @@ -193,8 +194,7 @@ def place_order(self, product_id, side, order_type, **kwargs): # Build params dict params = {'product_id': product_id, 'side': side, - 'type': order_type - } + 'type': order_type} params.update(kwargs) return self._send_message('post', '/orders', data=json.dumps(params)) @@ -284,7 +284,8 @@ def cancel_all(self, product_id=None): """ With best effort, cancel all open orders. Args: - product_id (Optional[str]): Only cancel ordrers for this product_id + product_id (Optional[str]): Only cancel orders for this + product_id Returns: list: A list of ids of the canceled orders. Example:: @@ -337,12 +338,12 @@ def get_order(self, order_id): """ return self._send_message('get', '/orders/' + order_id) - def get_orders(self, **kwargs): + def get_orders(self, product_id=None, status=None, **kwargs): """ List your current open orders. - Only open or un-settled orders are returned. As soon as an order - is no longer open and settled, it will no longer appear in the - default request. + Only open or un-settled orders are returned. As soon as an + order is no longer open and settled, it will no longer appear + in the default request. Orders which are no longer resting on the order book, will be marked with the 'done' status. There is a small window between @@ -357,11 +358,14 @@ def get_orders(self, **kwargs): the current state of any open orders. Args: - kwargs (dict): usage below - * status: Limit list of orders to these statuses. - Passing 'all' returns orders of all statuses. - ** default: ['open', 'pending', 'active'] - * product_id: Only list orders for a specific product + product_id (Optional[str]): Only list orders for this + product + status (Optional[list/str]): Limit list of orders to + this status or statuses. Passing 'all' returns orders + of all statuses. + ** Options: 'open', 'pending', 'active', 'done', + 'settled' + ** default: ['open', 'pending', 'active'] Returns: list: Containing information on orders. Example:: @@ -388,7 +392,12 @@ def get_orders(self, **kwargs): } ] """ - return self._send_message('get', '/orders', params=kwargs)[0] + params = kwargs + if product_id is not None: + params['product_id'] = product_id + if status is not None: + params['status'] = status + return self._send_paginated_message('/orders', params=kwargs) def get_fills(self, product_id=None, order_id=None, **kwargs): """ Get a list of recent fills. @@ -436,10 +445,7 @@ def get_fills(self, product_id=None, order_id=None, **kwargs): params['order_id'] = order_id params.update(kwargs) - # Return `after` param so client can access more recent fills on next - # call of get_fills if desired. - message, r = self._send_message('get', '/fills', params=params) - return r.headers['cb-after'], message + return self._send_paginated_message('/fills', params=params) def get_fundings(self, status=None, **kwargs): """ Every order placed with a margin profile that draws funding @@ -447,7 +453,7 @@ def get_fundings(self, status=None, **kwargs): Args: status (list/str): Limit funding records to these statuses. - ** Options: 'open', 'active', 'pending' + ** Options: 'outstanding', 'settled', 'rejected' kwargs (dict): Additional HTTP request parameters. Returns: @@ -474,7 +480,7 @@ def get_fundings(self, status=None, **kwargs): if status is not None: params['status'] = status params.update(kwargs) - return self._send_message('get', '/funding', params=params)[0] + return self._send_paginated_message('/funding', params=params) def repay_funding(self, amount, currency): """ Repay funding. Repays the older funding records first. @@ -495,22 +501,20 @@ def repay_funding(self, amount, currency): def margin_transfer(self, margin_profile_id, transfer_type, currency, amount): - params = { - 'margin_profile_id': margin_profile_id, - 'type': transfer_type, - 'currency': currency, # example: USD - 'amount': amount - } + params = {'margin_profile_id': margin_profile_id, + 'type': transfer_type, + 'currency': currency, # example: USD + 'amount': amount} return self._send_message('post', '/profiles/margin-transfer', data=json.dumps(params)) def get_position(self): - return self._send_message('get', '/position')[0] + return self._send_message('get', '/position') def close_position(self, repay_only): params = {'repay_only': repay_only} return self._send_message('post', '/position/close', - data=json.dumps(params))[0] + data=json.dumps(params)) def deposit(self, amount, currency, payment_method_id): """ Deposit funds from a payment method. @@ -519,7 +523,7 @@ def deposit(self, amount, currency, payment_method_id): information regarding payment methods. Args: - amount (int): The amount to depost. + amount (int): The amount to deposit. currency (str): The type of currency. payment_method_id (str): ID of the payment method. @@ -532,13 +536,11 @@ def deposit(self, amount, currency, payment_method_id): "payout_at": "2016-08-20T00:31:09Z" } """ - params = { - 'amount': amount, - 'currency': currency, - 'payment_method_id': payment_method_id - } + params = {'amount': amount, + 'currency': currency, + 'payment_method_id': payment_method_id} return self._send_message('post', '/deposits/payment-method', - data=json.dumps(params))[0] + data=json.dumps(params)) def coinbase_deposit(self, amount, currency, coinbase_account_id): """ Deposit funds from a coinbase account. @@ -551,7 +553,7 @@ def coinbase_deposit(self, amount, currency, coinbase_account_id): information regarding your coinbase_accounts. Args: - amount (int): The amount to depost. + amount (int): The amount to deposit. currency (str): The type of currency. coinbase_account_id (str): ID of the coinbase account. @@ -563,55 +565,45 @@ def coinbase_deposit(self, amount, currency, coinbase_account_id): "currency": "BTC", } """ - params = { - 'amount': amount, - 'currency': currency, - 'coinbase_account_id': coinbase_account_id - } + params = {'amount': amount, + 'currency': currency, + 'coinbase_account_id': coinbase_account_id} return self._send_message('post', '/deposits/coinbase-account', - data=json.dumps(params))[0] + data=json.dumps(params)) def withdraw(self, amount, currency, payment_method_id): - params = { - 'amount': amount, - 'currency': currency, - 'payment_method_id': payment_method_id - } + params = {'amount': amount, + 'currency': currency, + 'payment_method_id': payment_method_id} return self._send_message('post', '/withdrawals/payment-method', - data=json.dumps(params))[0] + data=json.dumps(params)) def coinbase_withdraw(self, amount, currency, coinbase_account_id): - params = { - 'amount': amount, - 'currency': currency, - 'coinbase_account_id': coinbase_account_id - } + params = {'amount': amount, + 'currency': currency, + 'coinbase_account_id': coinbase_account_id} return self._send_message('post', '/withdrawals/coinbase', - data=json.dumps(params))[0] + data=json.dumps(params)) def crypto_withdraw(self, amount, currency, crypto_address): - params = { - 'amount': amount, - 'currency': currency, - 'crypto_address': crypto_address - } + params = {'amount': amount, + 'currency': currency, + 'crypto_address': crypto_address} return self._send_message('post', '/withdrawals/crypto', - data=json.dumps(params))[0] + data=json.dumps(params)) def get_payment_methods(self): - return self._send_message('get', '/payment-methods')[0] + return self._send_message('get', '/payment-methods') def get_coinbase_accounts(self): - return self._send_message('get', '/coinbase-accounts')[0] + return self._send_message('get', '/coinbase-accounts') def create_report(self, report_type, start_date, end_date, product_id=None, account_id=None, report_format='pdf', email=None): - params = { - 'type': report_type, - 'start_date': start_date, - 'end_date': end_date, - 'format': report_format, - } + params = {'type': report_type, + 'start_date': start_date, + 'end_date': end_date, + 'format': report_format} if product_id is not None: params['product_id'] = product_id if account_id is not None: @@ -620,16 +612,16 @@ def create_report(self, report_type, start_date, end_date, product_id=None, params['email'] = email return self._send_message('post', '/reports', - data=json.dumps(params))[0] + data=json.dumps(params)) def get_report(self, report_id): - return self._send_message('get', '/reports/' + report_id)[0] + return self._send_message('get', '/reports/' + report_id) def get_trailing_volume(self): - return self._send_message('get', '/users/self/trailing-volume')[0] + return self._send_message('get', '/users/self/trailing-volume') def _send_message(self, method, endpoint, params=None, data=None): - """Get a paginated response by making multiple http requests. + """Send API request. Args: method (str): HTTP method (get, post, delete, etc.) @@ -638,26 +630,27 @@ def _send_message(self, method, endpoint, params=None, data=None): data (Optional[str]): JSON-encoded string payload for POST Returns: - list: Merged responses from paginated requests - requests.models.Response: Response object from last HTTP - response + dict/list: JSON response """ - if params is None: - params = {} - response_data = [] url = self.url + endpoint r = self.session.request(method, url, params=params, data=data, auth=self.auth) - if r.json(): - response_data = r.json() - if method == 'get': - while 'cb-after' in r.headers: + return r.json() + + def _send_paginated_message(self, endpoint, params=None): + url = self.url + endpoint + while True: + r = self.session.get(url, params=params, auth=self.auth) + results = r.json() + for result in results: + yield result + # If there are no more pages, we're done. Otherwise update `after` + # param to get next page + if not r.headers.get('cb-after'): + break + else: params['after'] = r.headers['cb-after'] - r = self.session.get(url, params=params, auth=self.auth) - if r.json(): - response_data += r.json() - return response_data, r class GdaxAuth(AuthBase): From be3382234b8406abe93d235c303e5db0c0a542aa Mon Sep 17 00:00:00 2001 From: acontry Date: Sun, 18 Jun 2017 22:13:48 -0700 Subject: [PATCH 046/174] Update documentation to match updated PublicClient There were references to default products which we just removed. Also: - Clean up some references to old CamelCase function names. - Format README.md to 80 character lines --- README.md | 170 +++++++++++++++++++++++++++++------------------------- 1 file changed, 93 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 400a33ed..5cd6240c 100644 --- a/README.md +++ b/README.md @@ -1,114 +1,103 @@ # GDAX-Python -The Python client for the [GDAX API](https://docs.gdax.com/) (formerly known as the Coinbase Exchange API) +The Python client for the [GDAX API](https://docs.gdax.com/) (formerly known as +the Coinbase Exchange API) ##### Provided under MIT License by Daniel Paquin. -*Note: this library may be subtly broken or buggy. The code is released under the MIT License – please take the following message to heart:* -> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*Note: this library may be subtly broken or buggy. The code is released under +the MIT License – please take the following message to heart:* +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## Benefits - A simple to use python wrapper for both public and authenticated endpoints. -- In about 10 minutes, you could be programmatically trading on one of the largest Bitcoin exchanges in the *world*! -- Do not worry about handling the nuances of the API with easy-to-use methods for every API endpoint. -- Gain an advantage in the market by getting under the hood of GDAX to learn what and who is *really* behind every tick. +- In about 10 minutes, you could be programmatically trading on one of the +largest Bitcoin exchanges in the *world*! +- Do not worry about handling the nuances of the API with easy-to-use methods +for every API endpoint. +- Gain an advantage in the market by getting under the hood of GDAX to learn +what and who is *really* behind every tick. ## Under Development -- Test Scripts **on dev branch** -- Additional Functionality for *WebsocketClient*, including a real-time order book +- Testing +- Additional Functionality for *WebsocketClient*, including a real-time order +book - FIX API Client **Looking for support** ## Getting Started -This README is documentation on the syntax of the python client presented in this repository. **In order to use this wrapper to its full potential, you must familiarize yourself with the official GDAX documentation.** +This README is documentation on the syntax of the python client presented in +this repository. See function docstrings for full syntax details. +**This API attempts to present a clean interface to GDAX, but n order to use it +to its full potential, you must familiarize yourself with the official GDAX +documentation.** - https://docs.gdax.com/ - You may manually install the project or use ```pip```: ```python -pip install GDAX +pip install gdax ``` ### Public Client -Only some endpoints in the API are available to everyone. The public endpoints can be reached using ```PublicClient``` +Only some endpoints in the API are available to everyone. The public endpoints +can be reached using ```PublicClient``` ```python import gdax public_client = gdax.PublicClient() -# Set a default product -public_client = gdax.PublicClient(product_id="ETH-USD") ``` ### PublicClient Methods -- [getProducts](https://docs.gdax.com/#get-products) +- [get_products](https://docs.gdax.com/#get-products) ```python public_client.get_products() ``` -- [getProductOrderBook](https://docs.gdax.com/#get-product-order-book) +- [get_product_order_book](https://docs.gdax.com/#get-product-order-book) ```python # Get the order book at the default level. -public_client.get_product_order_book() +public_client.get_product_order_book('BTC-USD') # Get the order book at a specific level. -public_client.get_product_order_book(level=1) +public_client.get_product_order_book('BTC-USD', level=1) ``` -- [getProductTicker](https://docs.gdax.com/#get-product-ticker) +- [get_product_ticker](https://docs.gdax.com/#get-product-ticker) ```python -# Get the product ticker for the default product. -public_client.get_product_ticker() # Get the product ticker for a specific product. -public_client.get_product_ticker(product="ETH-USD") +public_client.get_product_ticker(product_id='ETH-USD') ``` -- [getProductTrades](https://docs.gdax.com/#get-trades) +- [get_product_trades](https://docs.gdax.com/#get-trades) ```python -# Get the product trades for the default product. -public_client.get_product_trades() # Get the product trades for a specific product. -public_client.get_product_trades(product="ETH-USD") +public_client.get_product_trades(product_id='ETH-USD') ``` -- [getProductHistoricRates](https://docs.gdax.com/#get-historic-rates) +- [get_product_historic_rates](https://docs.gdax.com/#get-historic-rates) ```python -public_client.get_product_historic_rates() -# To include other parameters, see official documentation: -public_client.get_product_historic_rates(granularity=3000) +public_client.get_product_historic_rates('ETH-USD') +# To include other parameters, see function docstring: +public_client.get_product_historic_rates('ETH-USD', granularity=3000) ``` -- [getProduct24HrStates](https://docs.gdax.com/#get-24hr-stats) +- [get_product_24hr_stats](https://docs.gdax.com/#get-24hr-stats) ```python -public_client.get_product_24hr_stats() +public_client.get_product_24hr_stats('ETH-USD') ``` -- [getCurrencies](https://docs.gdax.com/#get-currencies) +- [get_currencies](https://docs.gdax.com/#get-currencies) ```python public_client.get_currencies() ``` -- [getTime](https://docs.gdax.com/#time) +- [get_time](https://docs.gdax.com/#time) ```python public_client.get_time() ``` -#### *In Development* JSON Parsing -Only available for the `PublicClient`, you may pass any function above raw JSON data. This may be useful for some applications of the project and should not hinder performance, but we are looking into this. *Do you love or hate this? Please share your thoughts within the issue tab!* - -- Both of these calls send the same request: -```python -import gdax -public_client = gdax.PublicClient() - -method1 = public_client.get_product_historic_rates(granularity='3000') - -params = { -'granularity': '3000' -} -method2 = public_client.get_product_historic_rates(params) - -# Both methods will send the same request, but not always return the same data if run in series. -print (method1, method2) -``` - - - ### Authenticated Client Not all API endpoints are available to everyone. @@ -123,37 +112,47 @@ integrate both into your script. import gdax auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase) # Set a default product -auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase, product_id="ETH-USD") +auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase, + product_id="ETH-USD") # Use the sandbox API (requires a different set of API access credentials) -auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase, api_url="https://api-public.sandbox.gdax.com") +auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase, + api_url="https://api-public.sandbox.gdax.com") ``` ### Pagination -Some calls are [paginated](https://docs.gdax.com/#pagination), meaning multiple calls must be made to receive the full set of data. Each page/request is a list of dict objects that are then appended to a master list, making it easy to navigate pages (e.g. ```request[0]``` would return the first page of data in the example below). *This feature is under consideration for redesign. Please provide feedback if you have issues or suggestions* +Some calls are [paginated](https://docs.gdax.com/#pagination), meaning multiple +calls must be made to receive the full set of data. Each page/request is a list +of dict objects that are then appended to a master list, making it easy to +navigate pages (e.g. ```request[0]``` would return the first page of data in the +example below). *This feature is under consideration for redesign. Please +provide feedback if you have issues or suggestions* ```python request = auth_client.get_fills(limit=100) request[0] # Page 1 always present request[1] # Page 2+ present only if the data exists ``` -It should be noted that limit does not behave exactly as the official documentation specifies. If you request a limit and that limit is met, additional pages will not be returned. This is to ensure speedy response times when less data is prefered. +It should be noted that limit does not behave exactly as the official +documentation specifies. If you request a limit and that limit is met, +additional pages will not be returned. This is to ensure speedy response times +when less data is preferred. ### AuthenticatedClient Methods -- [getAccounts](https://docs.gdax.com/#list-accounts) +- [get_accounts](https://docs.gdax.com/#list-accounts) ```python auth_client.get_accounts() ``` -- [getAccount](https://docs.gdax.com/#get-an-account) +- [get_account](https://docs.gdax.com/#get-an-account) ```python auth_client.get_account("7d0f7d8e-dd34-4d9c-a846-06f431c381ba") ``` -- [getAccountHistory](https://docs.gdax.com/#get-account-history) (paginated) +- [get_account_history](https://docs.gdax.com/#get-account-history) (paginated) ```python auth_client.get_account_history("7d0f7d8e-dd34-4d9c-a846-06f431c381ba") ``` -- [getAccountHolds](https://docs.gdax.com/#get-holds) (paginated) +- [get_account_holds](https://docs.gdax.com/#get-holds) (paginated) ```python auth_client.get_account_holds("7d0f7d8e-dd34-4d9c-a846-06f431c381ba") ``` @@ -172,26 +171,26 @@ auth_client.sell(price='200.00', #USD product_id='BTC-USD') ``` -- [cancelOrder](https://docs.gdax.com/#cancel-an-order) +- [cancel_order](https://docs.gdax.com/#cancel-an-order) ```python auth_client.cancel_order("d50ec984-77a8-460a-b958-66f114b0de9b") ``` -- [cancelAll](https://docs.gdax.com/#cancel-all) +- [cancel_all](https://docs.gdax.com/#cancel-all) ```python auth_client.cancel_all(product='BTC-USD') ``` -- [getOrders](https://docs.gdax.com/#list-orders) (paginated) +- [get_orders](https://docs.gdax.com/#list-orders) (paginated) ```python auth_client.get_orders() ``` -- [getOrder](https://docs.gdax.com/#get-an-order) +- [get_order](https://docs.gdax.com/#get-an-order) ```python auth_client.get_order("d50ec984-77a8-460a-b958-66f114b0de9b") ``` -- [getFills](https://docs.gdax.com/#list-fills) (paginated) +- [get_fills](https://docs.gdax.com/#list-fills) (paginated) ```python auth_client.get_fills() # Get fills for a specific order @@ -219,7 +218,8 @@ auth_client.withdraw(withdrawParams) ``` ### WebsocketClient -If you would like to receive real-time market updates, you must subscribe to the [websocket feed](https://docs.gdax.com/#websocket-feed). +If you would like to receive real-time market updates, you must subscribe to the +[websocket feed](https://docs.gdax.com/#websocket-feed). #### Subscribe to a single product ```python @@ -233,17 +233,23 @@ wsClient.close() #### Subscribe to multiple products ```python import gdax -# Paramters are optional -wsClient = gdax.WebsocketClient(url="wss://ws-feed.gdax.com", products=["BTC-USD", "ETH-USD"]) +# Paramaters are optional +wsClient = gdax.WebsocketClient(url="wss://ws-feed.gdax.com", + products=["BTC-USD", "ETH-USD"]) # Do other stuff... wsClient.close() ``` ### WebsocketClient Methods -The ```WebsocketClient``` subscribes in a separate thread upon initialization. There are three methods which you could overwrite (before initialization) so it can react to the data streaming in. The current client is a template used for illustration purposes only. - -- onOpen - called once, *immediately before* the socket connection is made, this is where you want to add inital parameters. -- onMessage - called once for every message that arrives and accepts one argument that contains the message of dict type. +The ```WebsocketClient``` subscribes in a separate thread upon initialization. +There are three methods which you could overwrite (before initialization) so it +can react to the data streaming in. The current client is a template used for +illustration purposes only. + +- onOpen - called once, *immediately before* the socket connection is made, this +is where you want to add inital parameters. +- onMessage - called once for every message that arrives and accepts one +argument that contains the message of dict type. - onClose - called once after the websocket has been closed. - close - call this method to close the websocket connection (do not overwrite). ```python @@ -257,7 +263,8 @@ class myWebsocketClient(gdax.WebsocketClient): def on_message(self, msg): self.message_count += 1 if 'price' in msg and 'type' in msg: - print ("Message type:", msg["type"], "\t@ {}.3f".format(float(msg["price"]))) + print ("Message type:", msg["type"], + "\t@ {}.3f".format(float(msg["price"]))) def on_close(self): print("-- Goodbye! --") @@ -269,9 +276,17 @@ while (wsClient.message_count < 500): time.sleep(1) wsClient.close() ``` +## Testing +A test suite is under development. To run the tests, start in the project +directory and run +``` +python -m pytest +``` ### Real-time OrderBook -The ```OrderBook``` subscribes to a websocket and keeps a real-time record of the orderbook for the product_id input. Please provide your feedback for future improvements. +The ```OrderBook``` subscribes to a websocket and keeps a real-time record of +the orderbook for the product_id input. Please provide your feedback for future +improvements. ```python import gdax, time @@ -293,7 +308,8 @@ order_book.close() - Added additional API functionality such as cancelAll() and ETH withdrawal. *0.2.1* -- Allowed ```WebsocketClient``` to operate intuitively and restructured example workflow. +- Allowed ```WebsocketClient``` to operate intuitively and restructured example +workflow. *0.2.0* - Renamed project to GDAX-Python From e1b47df57a17866488a0ca400e04299fd4008c8e Mon Sep 17 00:00:00 2001 From: acontry Date: Mon, 19 Jun 2017 01:49:02 -0700 Subject: [PATCH 047/174] Fix super() call in AuthenticatedClient The `product_id` parameter was just removed from the PublicClient class and the corresponding super() call in AuthenticatedClient wasn't updated. AuthenticatedClient throws an error upon instantiation as a result. --- gdax/authenticated_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index 20f3804f..4942ca7b 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -15,8 +15,8 @@ class AuthenticatedClient(PublicClient): - def __init__(self, key, b64secret, passphrase, api_url="https://api.gdax.com", product_id="BTC-USD"): - super(AuthenticatedClient, self).__init__(api_url, product_id) + def __init__(self, key, b64secret, passphrase, api_url="https://api.gdax.com"): + super(AuthenticatedClient, self).__init__(api_url) self.auth = GdaxAuth(key, b64secret, passphrase) def get_account(self, account_id): From 373a690571f0e12e0010a91d1681b284cfdce502 Mon Sep 17 00:00:00 2001 From: DJ Strasser Date: Thu, 22 Jun 2017 15:59:14 -0400 Subject: [PATCH 048/174] Change 'kwargs' -> 'params' Was just looking over your last commit and saw this small mistake, great work! I like the new generator approach to pagination. --- gdax/authenticated_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index 95601909..4dfe836a 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -397,7 +397,7 @@ def get_orders(self, product_id=None, status=None, **kwargs): params['product_id'] = product_id if status is not None: params['status'] = status - return self._send_paginated_message('/orders', params=kwargs) + return self._send_paginated_message('/orders', params=params) def get_fills(self, product_id=None, order_id=None, **kwargs): """ Get a list of recent fills. From 748a5cc947ebae63f55bd5f6c910309a70c83230 Mon Sep 17 00:00:00 2001 From: Dan Paquin Date: Thu, 22 Jun 2017 19:45:55 -0400 Subject: [PATCH 049/174] Updated docs for v1.0.0 --- README.md | 14 +++++++++----- setup.py | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5cd6240c..30d2766d 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,9 @@ for every API endpoint. what and who is *really* behind every tick. ## Under Development -- Testing -- Additional Functionality for *WebsocketClient*, including a real-time order -book -- FIX API Client **Looking for support** +- Test Scripts +- Additional Functionality for the real-time order book +- FIX API Client **Looking for assistance** ## Getting Started This README is documentation on the syntax of the python client presented in @@ -297,7 +296,12 @@ order_book.close() ``` ## Change Log -*0.3* **Current PyPI release** +*1.0* **Current PyPI release** +- The first release that is not backwards compatible +- Refactored to follow PEP 8 Standards +- Improved Documentation + +*0.3* - Added crypto and LTC deposit & withdraw (undocumented). - Added support for Margin trading (undocumented). - Enhanced functionality of the WebsocketClient. diff --git a/setup.py b/setup.py index 3372d753..64dec9bb 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ setup( name='gdax', - version='0.3.1', + version='1.0.0', author='Daniel Paquin', author_email='dpaq34@gmail.com', license='MIT', From e5d68b7b12150bd54a9b296885f68a2d66570f43 Mon Sep 17 00:00:00 2001 From: Dan Paquin Date: Thu, 22 Jun 2017 20:36:37 -0400 Subject: [PATCH 050/174] Updated files and refactored folder names for v1.0.6 --- README.md | 2 +- gdax/authenticated_client.py | 8 ++++---- gdax/order_book.py | 4 ++-- gdax/websocket_client.py | 4 ++-- setup.py | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 30d2766d..6ec6251a 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ auth_client.get_fills(product_id="ETH-BTC") - [deposit & withdraw](https://docs.gdax.com/#depositwithdraw) ```python -# Deposit into GDAX from Coinbase Wallet +gdax depositParams = { 'amount': '25.00', # Currency determined by account specified 'coinbase_account_id': '60680c98bfe96c2601f27e9c' diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index 4942ca7b..ec9df0c4 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -1,8 +1,8 @@ # -# GDAX/AuthenticatedClient.py +# gdax/AuthenticatedClient.py # Daniel Paquin # -# For authenticated requests to the GDAX exchange +# For authenticated requests to the gdax exchange import hmac import hashlib @@ -108,7 +108,7 @@ def get_orders(self): return result def paginate_orders(self, result, after): - r = requests.get(self.url + '/orders?after={}'.format(str(after))) + r = requests.get(self.url + '/orders?after={}'.format(str(after)), auth=self.auth) # r.raise_for_status() if r.json(): result.append(r.json()) @@ -285,7 +285,7 @@ def get_trailing_volume(self): class GdaxAuth(AuthBase): - # Provided by GDAX: https://docs.gdax.com/#signing-a-message + # Provided by gdax: https://docs.gdax.com/#signing-a-message def __init__(self, api_key, secret_key, passphrase): self.api_key = api_key self.secret_key = secret_key diff --git a/gdax/order_book.py b/gdax/order_book.py index 89a5d248..7b66a05f 100644 --- a/gdax/order_book.py +++ b/gdax/order_book.py @@ -1,8 +1,8 @@ # -# GDAX/OrderBook.py +# gdax/OrderBook.py # David Caseria # -# Live order book updated from the GDAX Websocket Feed +# Live order book updated from the gdax Websocket Feed from operator import itemgetter from bintrees import RBTree diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index 28633e32..0352dacc 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -1,8 +1,8 @@ # -# GDAX/WebsocketClient.py +# gdax/WebsocketClient.py # Daniel Paquin # -# Template object to receive messages from the GDAX Websocket Feed +# Template object to receive messages from the gdax Websocket Feed from __future__ import print_function import json diff --git a/setup.py b/setup.py index 64dec9bb..dbeee918 100644 --- a/setup.py +++ b/setup.py @@ -15,16 +15,16 @@ setup( name='gdax', - version='1.0.0', + version='1.0.6', author='Daniel Paquin', author_email='dpaq34@gmail.com', license='MIT', - url='https://github.com/danpaquin/GDAX-Python', + url='https://github.com/danpaquin/gdax-python', packages=find_packages(), install_requires=install_requires, tests_require=tests_require, description='The unofficial Python client for the GDAX API', - download_url='https://github.com/danpaquin/GDAX-Python/archive/master.zip', + download_url='https://github.com/danpaquin/gdax-Python/archive/master.zip', keywords=['gdax', 'gdax-api', 'orderbook', 'trade', 'bitcoin', 'ethereum', 'BTC', 'ETH', 'client', 'api', 'wrapper', 'exchange', 'crypto', 'currency', 'trading', 'trading-api', 'coinbase'], classifiers=[ 'Development Status :: 5 - Production/Stable', From 9e8a97232e70b74a6c928f51ab0bd402131d7454 Mon Sep 17 00:00:00 2001 From: Ted McCormack Date: Thu, 22 Jun 2017 19:03:23 -0700 Subject: [PATCH 051/174] Switch to list comprehension --- gdax/order_book.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gdax/order_book.py b/gdax/order_book.py index 7b66a05f..a6024e36 100644 --- a/gdax/order_book.py +++ b/gdax/order_book.py @@ -158,14 +158,14 @@ def change(self, order): bids = self.get_bids(price) if bids is None or not any(o['id'] == order['order_id'] for o in bids): return - index = map(itemgetter('id'), bids).index(order['order_id']) + index = [b['id'] for b in bids].index(order['order_id']) bids[index]['size'] = new_size self.set_bids(price, bids) else: asks = self.get_asks(price) if asks is None or not any(o['id'] == order['order_id'] for o in asks): return - index = map(itemgetter('id'), asks).index(order['order_id']) + index = [a['id'] for a in asks].index(order['order_id']) asks[index]['size'] = new_size self.set_asks(price, asks) From 277b0843224448012bc6ce1ec8b7ae877eaebe67 Mon Sep 17 00:00:00 2001 From: Paul Mestemaker Date: Fri, 23 Jun 2017 09:01:31 -0700 Subject: [PATCH 052/174] Addresses failures caused by recent refactor and WIP check-in: * Public client no longer accepts product_id * OrderBook still requires product_id because of its call to `get_product_order_book` * `log_to` doesn't exist --- gdax/order_book.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/gdax/order_book.py b/gdax/order_book.py index 7b66a05f..4a200969 100644 --- a/gdax/order_book.py +++ b/gdax/order_book.py @@ -12,18 +12,24 @@ from gdax.public_client import PublicClient from gdax.websocket_client import WebsocketClient + class OrderBook(WebsocketClient): - def __init__(self, product_id='BTC-USD'): + def __init__(self, product_id='BTC-USD', log_to=None): super(OrderBook, self).__init__(products=product_id) self._asks = RBTree() self._bids = RBTree() - self._client = PublicClient(product_id=product_id) + self._client = PublicClient() self._sequence = -1 self._log_to = log_to if self._log_to: assert hasattr(self._log_to, 'write') self._current_ticker = None + @property + def product_id(self): + ''' Currently OrderBook only supports a single product even though it is stored as a list of products. ''' + return self.products[0] + def on_message(self, message): if self._log_to: pickle.dump(message, self._log_to) @@ -32,7 +38,7 @@ def on_message(self, message): if self._sequence == -1: self._asks = RBTree() self._bids = RBTree() - res = self._client.get_product_order_book(level=3) + res = self._client.get_product_order_book(product_id=self.product_id, level=3) for bid in res['bids']: self.add({ 'id': bid[2], From 80501c0260b9e7cc145df25f236153716aafd662 Mon Sep 17 00:00:00 2001 From: Paul Mestemaker Date: Fri, 23 Jun 2017 09:36:33 -0700 Subject: [PATCH 053/174] New class: OrderBookConsole * Logs real-time changes to the bid-ask spread to the console --- gdax/order_book.py | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/gdax/order_book.py b/gdax/order_book.py index 4a200969..bc415259 100644 --- a/gdax/order_book.py +++ b/gdax/order_book.py @@ -1,5 +1,5 @@ # -# gdax/OrderBook.py +# gdax/order_book.py # David Caseria # # Live order book updated from the gdax Websocket Feed @@ -238,8 +238,45 @@ def set_bids(self, price, bids): if __name__ == '__main__': import time + import datetime as dt - order_book = OrderBook() + + class OrderBookConsole(OrderBook): + ''' Logs real-time changes to the bid-ask spread to the console ''' + + def __init__(self, product_id=None): + super(OrderBookConsole, self).__init__(product_id=product_id) + + # latest values of bid-ask spread + self._bid = None + self._ask = None + self._bid_depth = None + self._ask_depth = None + + def on_message(self, message): + super(OrderBookConsole, self).on_message(message) + + # Calculate newest bid-ask spread + bid = self.get_bid() + bids = self.get_bids(bid) + bid_depth = sum([b['size'] for b in bids]) + ask = self.get_ask() + asks = self.get_asks(ask) + ask_depth = sum([a['size'] for a in asks]) + + if self._bid == bid and self._ask == ask and self._bid_depth == bid_depth and self._ask_depth == ask_depth: + # If there are no changes to the bid-ask spread since the last update, no need to print + pass + else: + # If there are differences, update the cache + self._bid = bid + self._ask = ask + self._bid_depth = bid_depth + self._ask_depth = ask_depth + print('{}\tbid: {:.3f} @ {:.2f}\task: {:.3f} @ {:.2f}'.format(dt.datetime.now(), bid_depth, bid, + ask_depth, ask)) + + order_book = OrderBookConsole() order_book.start() time.sleep(10) order_book.close() From bca8126b551966a545adc848cbd8590f5f0bfed2 Mon Sep 17 00:00:00 2001 From: Ted McCormack Date: Mon, 26 Jun 2017 23:12:48 -0700 Subject: [PATCH 054/174] Remove itemgetter --- gdax/order_book.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gdax/order_book.py b/gdax/order_book.py index a6024e36..92e182f2 100644 --- a/gdax/order_book.py +++ b/gdax/order_book.py @@ -4,7 +4,6 @@ # # Live order book updated from the gdax Websocket Feed -from operator import itemgetter from bintrees import RBTree from decimal import Decimal import pickle From 2327a1349cc948fcbd057f0eb6b245f45c077e56 Mon Sep 17 00:00:00 2001 From: acontry Date: Sat, 1 Jul 2017 23:44:13 -0700 Subject: [PATCH 055/174] Fix paginated generator, add docstrings and tests. --- gdax/authenticated_client.py | 374 +++++++++++++++++++++++++++-- tests/api_config.json.example | 3 + tests/test_authenticated_client.py | 153 +++++++++++- 3 files changed, 515 insertions(+), 15 deletions(-) create mode 100644 tests/api_config.json.example diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index 4dfe836a..f87ab13b 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -128,8 +128,13 @@ def get_account_history(self, account_id, **kwargs): return self._send_paginated_message(endpoint, params=kwargs) def get_account_holds(self, account_id, **kwargs): - """ Holds are placed on an account for active orders or pending - withdraw requests. + """ Get holds on an account. + + This method returns a generator which may make multiple HTTP requests + while iterating through it. + + Holds are placed on an account for active orders or + pending withdraw requests. As an order is filled, the hold amount is updated. If an order is canceled, any remaining hold is removed. For a withdraw, once @@ -147,7 +152,7 @@ def get_account_holds(self, account_id, **kwargs): kwargs (dict): Additional HTTP request parameters. Returns: - list: Hold information for the provided account. Example:: + generator(list): Hold information for the account. Example:: [ { "id": "82dcd140-c3c7-4507-8de4-2c529cd1a28f", @@ -162,14 +167,65 @@ def get_account_holds(self, account_id, **kwargs): ... } ] + """ endpoint = '/accounts/{}/holds'.format(account_id) return self._send_paginated_message(endpoint, params=kwargs) def place_order(self, product_id, side, order_type, **kwargs): + """ Place an order. + + The three order types (limit, market, and stop) can be placed using this + method. Specific methods are provided for each order type, but if a + more generic interface is desired this method is available. + + Args: + product_id (str): Product to order (eg. 'BTC-USD') + side (str): Order side ('buy' or 'sell) + order_type (str): Order type ('limit', 'market', or 'stop') + **client_oid (str): Order ID selected by you to identify your order. + This should be a UUID, which will be broadcast in the public + feed for `received` messages. + **stp (str): Self-trade prevention flag. GDAX doesn't allow self- + trading. This behavior can be modified with this flag. + Options: + 'dc' Decrease and Cancel (default) + 'co' Cancel oldest + 'cn' Cancel newest + 'cb' Cancel both + **overdraft_enabled (Optional[bool]): If true funding above and + beyond the account balance will be provided by margin, as + necessary. + **funding_amount (Optional[Decimal]): Amount of margin funding to be + provided for the order. Mutually exclusive with + `overdraft_enabled`. + **kwargs: Additional arguments can be specified for different order + types. See the limit/market/stop order methods for details. + + Returns: + dict: Order details. Example:: + { + "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2", + "price": "0.10000000", + "size": "0.01000000", + "product_id": "BTC-USD", + "side": "buy", + "stp": "dc", + "type": "limit", + "time_in_force": "GTC", + "post_only": false, + "created_at": "2016-12-08T20:02:28.53864Z", + "fill_fees": "0.0000000000000000", + "filled_size": "0.00000000", + "executed_value": "0.0000000000000000", + "status": "pending", + "settled": false + } + + """ # Margin parameter checks if kwargs.get('overdraft_enabled') is not None and \ - kwargs.get('funding_amount') is not None: + kwargs.get('funding_amount') is not None: raise ValueError('Margin funding must be specified through use of ' 'overdraft or by setting a funding amount, but not' ' both') @@ -177,7 +233,7 @@ def place_order(self, product_id, side, order_type, **kwargs): # Limit order checks if order_type == 'limit': if kwargs.get('cancel_after') is not None and \ - kwargs.get('tif') != 'GTT': + kwargs.get('tif') != 'GTT': raise ValueError('May only specify a cancel period when time ' 'in_force is `GTT`') if kwargs.get('post_only') is not None and kwargs.get('tif') in \ @@ -201,11 +257,43 @@ def place_order(self, product_id, side, order_type, **kwargs): def place_limit_order(self, product_id, side, price, size, client_oid=None, stp=None, - tif=None, + time_in_force=None, cancel_after=None, post_only=None, overdraft_enabled=None, funding_amount=None): + """Place a limit order. + + Args: + product_id (str): Product to order (eg. 'BTC-USD') + side (str): Order side ('buy' or 'sell) + price (Decimal): Price per cryptocurrency + size (Decimal): Amount of cryptocurrency to buy or sell + client_oid (Optional[str]): User-specified Order ID + stp (Optional[str]): Self-trade prevention flag. See `place_order` + for details. + time_in_force (Optional[str]): Time in force. Options: + 'GTC' Good till canceled + 'GTT' Good till time (set by `cancel_after`) + 'IOC' Immediate or cancel + 'FOK' Fill or kill + cancel_after (Optional[str]): Cancel after this period for 'GTT' + orders. Options are 'min', 'hour', or 'day'. + post_only (Optional[bool]): Indicates that the order should only + make liquidity. If any part of the order results in taking + liquidity, the order will be rejected and no part of it will + execute. + overdraft_enabled (Optional[bool]): If true funding above and + beyond the account balance will be provided by margin, as + necessary. + funding_amount (Optional[Decimal]): Amount of margin funding to be + provided for the order. Mutually exclusive with + `overdraft_enabled`. + + Returns: + dict: Order details. See `place_order` for example. + + """ params = {'product_id': product_id, 'side': side, 'order_type': 'limit', @@ -213,7 +301,7 @@ def place_limit_order(self, product_id, side, price, size, 'size': size, 'client_oid': client_oid, 'stp': stp, - 'tif': tif, + 'time_in_force': time_in_force, 'cancel_after': cancel_after, 'post_only': post_only, 'overdraft_enabled': overdraft_enabled, @@ -222,11 +310,34 @@ def place_limit_order(self, product_id, side, price, size, return self.place_order(**params) - def place_market_order(self, product_id, side, size, funds, + def place_market_order(self, product_id, side, size=None, funds=None, client_oid=None, stp=None, overdraft_enabled=None, funding_amount=None): + """ Place market order. + + Args: + product_id (str): Product to order (eg. 'BTC-USD') + side (str): Order side ('buy' or 'sell) + size (Optional[Decimal]): Desired amount in crypto. Specify this or + `funds`. + funds (Optional[Decimal]): Desired amount of quote currency to use. + Specify this or `size`. + client_oid (Optional[str]): User-specified Order ID + stp (Optional[str]): Self-trade prevention flag. See `place_order` + for details. + overdraft_enabled (Optional[bool]): If true funding above and + beyond the account balance will be provided by margin, as + necessary. + funding_amount (Optional[Decimal]): Amount of margin funding to be + provided for the order. Mutually exclusive with + `overdraft_enabled`. + + Returns: + dict: Order details. See `place_order` for example. + + """ params = {'product_id': product_id, 'side': side, 'order_type': 'market', @@ -240,11 +351,35 @@ def place_market_order(self, product_id, side, size, funds, return self.place_order(**params) - def place_stop_order(self, product_id, side, price, size, funds, + def place_stop_order(self, product_id, side, price, size=None, funds=None, client_oid=None, stp=None, overdraft_enabled=None, funding_amount=None): + """ Place stop order. + + Args: + product_id (str): Product to order (eg. 'BTC-USD') + side (str): Order side ('buy' or 'sell) + price (Decimal): Desired price at which the stop order triggers. + size (Optional[Decimal]): Desired amount in crypto. Specify this or + `funds`. + funds (Optional[Decimal]): Desired amount of quote currency to use. + Specify this or `size`. + client_oid (Optional[str]): User-specified Order ID + stp (Optional[str]): Self-trade prevention flag. See `place_order` + for details. + overdraft_enabled (Optional[bool]): If true funding above and + beyond the account balance will be provided by margin, as + necessary. + funding_amount (Optional[Decimal]): Amount of margin funding to be + provided for the order. Mutually exclusive with + `overdraft_enabled`. + + Returns: + dict: Order details. See `place_order` for example. + + """ params = {'product_id': product_id, 'side': side, 'price': price, @@ -277,6 +412,7 @@ def cancel_order(self, order_id): Returns: list: Containing the order_id of cancelled order. Example:: [ "c5ab5eae-76be-480e-8961-00792dc7e138" ] + """ return self._send_message('delete', '/orders/' + order_id) @@ -296,6 +432,7 @@ def cancel_all(self, product_id=None): "dfc5ae27-cadb-4c0c-beef-8994936fde8a", "34fecfbf-de33-4273-b2c6-baf8e8948be4" ] + """ if product_id is not None: params = {'product_id': product_id} @@ -335,12 +472,16 @@ def get_order(self, order_id): "time_in_force": "GTC", "type": "limit" } + """ return self._send_message('get', '/orders/' + order_id) def get_orders(self, product_id=None, status=None, **kwargs): """ List your current open orders. + This method returns a generator which may make multiple HTTP requests + while iterating through it. + Only open or un-settled orders are returned. As soon as an order is no longer open and settled, it will no longer appear in the default request. @@ -391,6 +532,7 @@ def get_orders(self, product_id=None, status=None, **kwargs): ... } ] + """ params = kwargs if product_id is not None: @@ -402,6 +544,9 @@ def get_orders(self, product_id=None, status=None, **kwargs): def get_fills(self, product_id=None, order_id=None, **kwargs): """ Get a list of recent fills. + This method returns a generator which may make multiple HTTP requests + while iterating through it. + Fees are recorded in two stages. Immediately after the matching engine completes a match, the fill is inserted into our datastore. Once the fill is recorded, a settlement process will @@ -437,6 +582,7 @@ def get_fills(self, product_id=None, order_id=None, **kwargs): ... } ] + """ params = {} if product_id: @@ -451,6 +597,9 @@ def get_fundings(self, status=None, **kwargs): """ Every order placed with a margin profile that draws funding will create a funding record. + This method returns a generator which may make multiple HTTP requests + while iterating through it. + Args: status (list/str): Limit funding records to these statuses. ** Options: 'outstanding', 'settled', 'rejected' @@ -475,6 +624,7 @@ def get_fundings(self, status=None, **kwargs): ... } ] + """ params = {} if status is not None: @@ -490,7 +640,8 @@ def repay_funding(self, amount, currency): currency (str): The currency, example USD Returns: - ??? + Not specified by GDAX. + """ params = { 'amount': amount, @@ -501,6 +652,34 @@ def repay_funding(self, amount, currency): def margin_transfer(self, margin_profile_id, transfer_type, currency, amount): + """ Transfer funds between your standard profile and a margin profile. + + Args: + margin_profile_id (str): Margin profile ID to withdraw or deposit + from. + transfer_type (str): 'deposit' or 'withdraw' + currency (str): Currency to transfer (eg. 'USD') + amount (Decimal): Amount to transfer + + Returns: + dict: Transfer details. Example:: + { + "created_at": "2017-01-25T19:06:23.415126Z", + "id": "80bc6b74-8b1f-4c60-a089-c61f9810d4ab", + "user_id": "521c20b3d4ab09621f000011", + "profile_id": "cda95996-ac59-45a3-a42e-30daeb061867", + "margin_profile_id": "45fa9e3b-00ba-4631-b907-8a98cbdf21be", + "type": "deposit", + "amount": "2", + "currency": "USD", + "account_id": "23035fc7-0707-4b59-b0d2-95d0c035f8f5", + "margin_account_id": "e1d9862c-a259-4e83-96cd-376352a9d24d", + "margin_product_id": "BTC-USD", + "status": "completed", + "nonce": 25 + } + + """ params = {'margin_profile_id': margin_profile_id, 'type': transfer_type, 'currency': currency, # example: USD @@ -509,9 +688,24 @@ def margin_transfer(self, margin_profile_id, transfer_type, currency, data=json.dumps(params)) def get_position(self): + """ Get An overview of your margin profile. + + Returns: + dict: Details about funding, accounts, and margin call. + + """ return self._send_message('get', '/position') def close_position(self, repay_only): + """ Close position. + + Args: + repay_only (bool): Undocumented by GDAX. + + Returns: + Undocumented + + """ params = {'repay_only': repay_only} return self._send_message('post', '/position/close', data=json.dumps(params)) @@ -523,7 +717,7 @@ def deposit(self, amount, currency, payment_method_id): information regarding payment methods. Args: - amount (int): The amount to deposit. + amount (Decmial): The amount to deposit. currency (str): The type of currency. payment_method_id (str): ID of the payment method. @@ -535,6 +729,7 @@ def deposit(self, amount, currency, payment_method_id): "currency": "USD", "payout_at": "2016-08-20T00:31:09Z" } + """ params = {'amount': amount, 'currency': currency, @@ -553,7 +748,7 @@ def coinbase_deposit(self, amount, currency, coinbase_account_id): information regarding your coinbase_accounts. Args: - amount (int): The amount to deposit. + amount (Decimal): The amount to deposit. currency (str): The type of currency. coinbase_account_id (str): ID of the coinbase account. @@ -564,6 +759,7 @@ def coinbase_deposit(self, amount, currency, coinbase_account_id): "amount": "10.00", "currency": "BTC", } + """ params = {'amount': amount, 'currency': currency, @@ -572,6 +768,26 @@ def coinbase_deposit(self, amount, currency, coinbase_account_id): data=json.dumps(params)) def withdraw(self, amount, currency, payment_method_id): + """ Withdraw funds to a payment method. + + See AuthenticatedClient.get_payment_methods() to receive + information regarding payment methods. + + Args: + amount (Decimal): The amount to withdraw. + currency (str): Currency type (eg. 'BTC') + payment_method_id (str): ID of the payment method. + + Returns: + dict: Withdraw details. Example:: + { + "id":"593533d2-ff31-46e0-b22e-ca754147a96a", + "amount": "10.00", + "currency": "USD", + "payout_at": "2016-08-20T00:31:09Z" + } + + """ params = {'amount': amount, 'currency': currency, 'payment_method_id': payment_method_id} @@ -579,6 +795,29 @@ def withdraw(self, amount, currency, payment_method_id): data=json.dumps(params)) def coinbase_withdraw(self, amount, currency, coinbase_account_id): + """ Withdraw funds to a coinbase account. + + You can move funds between your Coinbase accounts and your GDAX + trading accounts within your daily limits. Moving funds between + Coinbase and GDAX is instant and free. + + See AuthenticatedClient.get_coinbase_accounts() to receive + information regarding your coinbase_accounts. + + Args: + amount (Decimal): The amount to withdraw. + currency (str): The type of currency (eg. 'BTC') + coinbase_account_id (str): ID of the coinbase account. + + Returns: + dict: Information about the deposit. Example:: + { + "id":"593533d2-ff31-46e0-b22e-ca754147a96a", + "amount":"10.00", + "currency": "BTC", + } + + """ params = {'amount': amount, 'currency': currency, 'coinbase_account_id': coinbase_account_id} @@ -586,6 +825,22 @@ def coinbase_withdraw(self, amount, currency, coinbase_account_id): data=json.dumps(params)) def crypto_withdraw(self, amount, currency, crypto_address): + """ Withdraw funds to a crypto address. + + Args: + amount (Decimal): The amount to withdraw + currency (str): The type of currency (eg. 'BTC') + crypto_address (str): Crypto address to withdraw to. + + Returns: + dict: Withdraw details. Example:: + { + "id":"593533d2-ff31-46e0-b22e-ca754147a96a", + "amount":"10.00", + "currency": "BTC", + } + + """ params = {'amount': amount, 'currency': currency, 'crypto_address': crypto_address} @@ -593,13 +848,58 @@ def crypto_withdraw(self, amount, currency, crypto_address): data=json.dumps(params)) def get_payment_methods(self): + """ Get a list of your payment methods. + + Returns: + list: Payment method details. + + """ return self._send_message('get', '/payment-methods') def get_coinbase_accounts(self): + """ Get a list of your coinbase accounts. + + Returns: + list: Coinbase account details. + + """ return self._send_message('get', '/coinbase-accounts') def create_report(self, report_type, start_date, end_date, product_id=None, account_id=None, report_format='pdf', email=None): + """ Create report of historic information about your account. + + The report will be generated when resources are available. Report status + can be queried via `get_report(report_id)`. + + Args: + report_type (str): 'fills' or 'account' + start_date (str): Starting date for the report in ISO 8601 + end_date (str): Ending date for the report in ISO 8601 + product_id (Optional[str]): ID of the product to generate a fills + report for. Required if account_type is 'fills' + account_id (Optional[str]): ID of the account to generate an account + report for. Required if report_type is 'account'. + report_format (Optional[str]): 'pdf' or 'csv'. Default is 'pdf'. + email (Optional[str]): Email address to send the report to. + + Returns: + dict: Report details. Example:: + { + "id": "0428b97b-bec1-429e-a94c-59232926778d", + "type": "fills", + "status": "pending", + "created_at": "2015-01-06T10:34:47.000Z", + "completed_at": undefined, + "expires_at": "2015-01-13T10:35:47.000Z", + "file_url": undefined, + "params": { + "start_date": "2014-11-01T00:00:00.000Z", + "end_date": "2014-11-30T23:59:59.000Z" + } + } + + """ params = {'type': report_type, 'start_date': start_date, 'end_date': end_date, @@ -615,9 +915,39 @@ def create_report(self, report_type, start_date, end_date, product_id=None, data=json.dumps(params)) def get_report(self, report_id): + """ Get report status. + + Use to query a specific report once it has been requested. + + Args: + report_id (str): Report ID + + Returns: + dict: Report details, including file url once it is created. + + """ return self._send_message('get', '/reports/' + report_id) def get_trailing_volume(self): + """ Get your 30-day trailing volume for all products. + + This is a cached value that’s calculated every day at midnight UTC. + + Returns: + list: 30-day trailing volumes. Example:: + [ + { + "product_id": "BTC-USD", + "exchange_volume": "11800.00000000", + "volume": "100.00000000", + "recorded_at": "1973-11-29T00:05:01.123456Z" + }, + { + ... + } + ] + + """ return self._send_message('get', '/users/self/trailing-volume') def _send_message(self, method, endpoint, params=None, data=None): @@ -639,6 +969,19 @@ def _send_message(self, method, endpoint, params=None, data=None): return r.json() def _send_paginated_message(self, endpoint, params=None): + """ Send API message that results in a paginated response. + + The paginated responses are abstracted away by making API requests on + demand as the response is iterated over. + + Args: + endpoint (str): Endpoint (to be added to base URL) + params (Optional[dict]): HTTP request parameters + + Yields: + dict: API response objects + + """ url = self.url + endpoint while True: r = self.session.get(url, params=params, auth=self.auth) @@ -646,8 +989,11 @@ def _send_paginated_message(self, endpoint, params=None): for result in results: yield result # If there are no more pages, we're done. Otherwise update `after` - # param to get next page - if not r.headers.get('cb-after'): + # param to get next page. + # If this request included `before` don't get any more pages - the + # GDAX API doesn't support multiple pages in that case. + if not r.headers.get('cb-after') or \ + params.get('before') is not None: break else: params['after'] = r.headers['cb-after'] diff --git a/tests/api_config.json.example b/tests/api_config.json.example new file mode 100644 index 00000000..12d90097 --- /dev/null +++ b/tests/api_config.json.example @@ -0,0 +1,3 @@ +{"passphrase": "passhere", + "b64secret": "secrethere", + "key": "key here"} \ No newline at end of file diff --git a/tests/test_authenticated_client.py b/tests/test_authenticated_client.py index b9ebb1b7..96f034f7 100644 --- a/tests/test_authenticated_client.py +++ b/tests/test_authenticated_client.py @@ -1,4 +1,7 @@ import pytest +import json +import time +from itertools import islice import gdax @@ -9,7 +12,7 @@ def dc(): @pytest.mark.usefixtures('dc') -class TestAuthenticatedClient(object): +class TestAuthenticatedClientSyntax(object): def test_place_order_input_1(self, dc): with pytest.raises(ValueError): r = dc.place_order('BTC-USD', 'buy', 'market', @@ -34,3 +37,151 @@ def test_place_order_input_5(self, dc): with pytest.raises(ValueError): r = dc.place_order('BTC-USD', 'buy', 'market', size=1, funds=1) + + +@pytest.fixture(scope='module') +def client(): + """Client that connects to sandbox API. Relies on authentication information + provided in api_config.json""" + with open('api_config.json') as file: + api_config = json.load(file) + c = gdax.AuthenticatedClient( + api_url='https://api-public.sandbox.gdax.com', **api_config) + + # Set up account with deposits and orders. Do this by depositing from + # the Coinbase USD wallet, which has a fixed value of > $10,000. + # + # Only deposit if the balance is below some nominal amount. The + # exchange seems to freak out if you run up your account balance. + coinbase_accounts = c.get_coinbase_accounts() + account_info = [x for x in coinbase_accounts + if x['name'] == 'USD Wallet'][0] + account_usd = account_info['id'] + if float(account_info['balance']) < 70000: + c.coinbase_deposit(10000, 'USD', account_usd) + # Place some orders to generate history + c.place_limit_order('BTC-USD', 'buy', 1, 0.01) + c.place_limit_order('BTC-USD', 'buy', 2, 0.01) + c.place_limit_order('BTC-USD', 'buy', 3, 0.01) + + return c + + +@pytest.mark.usefixtures('dc') +class TestAuthenticatedClient(object): + """Test the authenticated client by validating basic behavior from the + sandbox exchange.""" + def test_get_accounts(self, client): + r = client.get_accounts() + assert type(r) is list + assert 'currency' in r[0] + # Now get a single account + r = client.get_account(account_id=r[0]['id']) + assert type(r) is dict + assert 'currency' in r + + def test_account_history(self, client): + accounts = client.get_accounts() + account_usd = [x for x in accounts if x['currency'] == 'USD'][0]['id'] + r = list(islice(client.get_account_history(account_usd), 5)) + assert type(r) is list + assert 'amount' in r[0] + assert 'details' in r[0] + + # Now exercise the pagination abstraction. Setting limit to 1 means + # each record comes in a separate HTTP response. + history_gen = client.get_account_history(account_usd, limit=1) + r = list(islice(history_gen, 2)) + r2 = list(islice(history_gen, 2)) + assert r != r2 + # Now exercise the `before` parameter. + r3 = list(client.get_account_history(account_usd, before=r2[0]['id'])) + assert r3 == r + + def test_get_account_holds(self, client): + accounts = client.get_accounts() + account_usd = [x for x in accounts if x['currency'] == 'USD'][0]['id'] + r = list(client.get_account_holds(account_usd)) + assert type(r) is list + assert 'type' in r[0] + assert 'ref' in r[0] + + def test_place_order(self, client): + r = client.place_order('BTC-USD', 'buy', 'limit', + price=0.62, size=0.0144) + assert type(r) is dict + assert r['stp'] == 'dc' + + def test_place_limit_order(self, client): + r = client.place_limit_order('BTC-USD', 'buy', 4.43, 0.01232) + assert type(r) is dict + assert 'executed_value' in r + assert not r['post_only'] + client.cancel_order(r['id']) + + def test_place_market_order(self, client): + r = client.place_market_order('BTC-USD', 'buy', size=0.01) + assert 'status' in r + assert r['type'] == 'market' + client.cancel_order(r['id']) + + # This one probably won't go through + r = client.place_market_order('BTC-USD', 'buy', funds=100000) + assert type(r) is dict + + def test_place_stop_order(self, client): + client.cancel_all() + r = client.place_stop_order('BTC-USD', 'buy', 1, 0.01) + assert type(r) is dict + assert r['type'] == 'stop' + client.cancel_order(r['id']) + + def test_cancel_order(self, client): + r = client.place_limit_order('BTC-USD', 'buy', 4.43, 0.01232) + time.sleep(0.2) + r2 = client.cancel_order(r['id']) + assert r2[0] == r['id'] + + def test_cancel_all(self, client): + r = client.cancel_all() + assert type(r) is list + + def test_get_order(self, client): + r = client.place_limit_order('BTC-USD', 'buy', 4.43, 0.01232) + time.sleep(0.2) + r2 = client.get_order(r['id']) + assert r2['id'] == r['id'] + + def test_get_orders(self, client): + r = list(islice(client.get_orders(), 10)) + assert type(r) is list + assert 'created_at' in r[0] + + def test_get_fills(self, client): + r = list(islice(client.get_orders(), 10)) + assert type(r) is list + assert 'fill_fees' in r[0] + + def test_get_fundings(self, client): + r = list(islice(client.get_fundings(), 10)) + assert type(r) is list + + def test_repay_funding(self, client): + # This request gets denied + r = client.repay_funding(2.1, 'USD') + + def test_get_position(self, client): + r = client.get_position() + assert 'accounts' in r + + def test_get_payment_methods(self, client): + r = client.get_payment_methods() + assert type(r) is list + + def test_get_coinbase_accounts(self, client): + r = client.get_coinbase_accounts() + assert type(r) is list + + def test_get_trailing_volume(self, client): + r = client.get_trailing_volume() + assert type(r) is list From 6d5a7f9150ebbbf0b2634a5eeb2f1c60fed01b41 Mon Sep 17 00:00:00 2001 From: acontry Date: Wed, 5 Jul 2017 21:18:02 -0700 Subject: [PATCH 056/174] Update README to explain updated authenticated API Also add docstring comments about paginated request paramters. --- README.md | 46 +++++++++++++++++++++++------------- gdax/authenticated_client.py | 9 +++++++ 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 46c04a29..45db556d 100644 --- a/README.md +++ b/README.md @@ -110,9 +110,6 @@ integrate both into your script. ```python import gdax auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase) -# Set a default product -auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase, - product_id="ETH-USD") # Use the sandbox API (requires a different set of API access credentials) auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase, api_url="https://api-public.sandbox.gdax.com") @@ -120,20 +117,29 @@ auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase, ### Pagination Some calls are [paginated](https://docs.gdax.com/#pagination), meaning multiple -calls must be made to receive the full set of data. Each page/request is a list -of dict objects that are then appended to a master list, making it easy to -navigate pages (e.g. ```request[0]``` would return the first page of data in the -example below). *This feature is under consideration for redesign. Please -provide feedback if you have issues or suggestions* +calls must be made to receive the full set of data. The GDAX Python API provides +an abstraction for paginated endpoints in the form of generators which provide a +clean interface for iteration but may make multiple HTTP requests behind the +scenes. The pagination options `before`, `after`, and `limit` may be supplied as +keyword arguments if desired, but aren't necessary for typical use cases. +```python +fills_gen = auth_client.get_fills() +# Get all fills (will possibly make multiple HTTP requests) +all_fills = list(fills_gen) +``` +One use case for pagination parameters worth pointing out is retrieving only +new data since the previous request. For the case of `get_fills()`, the +`trade_id` is the parameter used for indexing. By passing +`before=some_trade_id`, only fills more recent than that `trade_id` will be +returned. Note that when using `before`, a maximum of 100 entries will be +returned - this is a limitation of GDAX. ```python -request = auth_client.get_fills(limit=100) -request[0] # Page 1 always present -request[1] # Page 2+ present only if the data exists +from itertools import islice +# Get 5 most recent fills +recent_fills = islice(auth_client.get_fills(), 5) +# Only fetch new fills since last call by utilizing `before` parameter. +new_fills = auth_client.get_fills(before=recent_fills[0]['trade_id']) ``` -It should be noted that limit does not behave exactly as the official -documentation specifies. If you request a limit and that limit is met, -additional pages will not be returned. This is to ensure speedy response times -when less data is preferred. ### AuthenticatedClient Methods - [get_accounts](https://docs.gdax.com/#list-accounts) @@ -148,11 +154,13 @@ auth_client.get_account("7d0f7d8e-dd34-4d9c-a846-06f431c381ba") - [get_account_history](https://docs.gdax.com/#get-account-history) (paginated) ```python +# Returns generator: auth_client.get_account_history("7d0f7d8e-dd34-4d9c-a846-06f431c381ba") ``` - [get_account_holds](https://docs.gdax.com/#get-holds) (paginated) ```python +# Returns generator: auth_client.get_account_holds("7d0f7d8e-dd34-4d9c-a846-06f431c381ba") ``` @@ -181,6 +189,7 @@ auth_client.cancel_all(product='BTC-USD') - [get_orders](https://docs.gdax.com/#list-orders) (paginated) ```python +# Returns generator: auth_client.get_orders() ``` @@ -191,6 +200,7 @@ auth_client.get_order("d50ec984-77a8-460a-b958-66f114b0de9b") - [get_fills](https://docs.gdax.com/#list-fills) (paginated) ```python +# All return generators auth_client.get_fills() # Get fills for a specific order auth_client.get_fills(order_id="d50ec984-77a8-460a-b958-66f114b0de9b") @@ -276,8 +286,10 @@ while (wsClient.message_count < 500): wsClient.close() ``` ## Testing -A test suite is under development. To run the tests, start in the project -directory and run +A test suite is under development. Tests for the authenticated client require a +set of sandbox API credentials. To provide them, rename +`api_config.json.example` in the tests folder to `api_config.json` and edit the +file accordingly. To run the tests, start in the project directory and run ``` python -m pytest ``` diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index 2099444b..b3c79a50 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -974,6 +974,15 @@ def _send_paginated_message(self, endpoint, params=None): The paginated responses are abstracted away by making API requests on demand as the response is iterated over. + Paginated API messages support 3 additional parameters: `before`, + `after`, and `limit`. `before` and `after` are mutually exclusive. To + use them, supply an index value for that endpoint (the field used for + indexing varies by endpoint - get_fills() uses 'trade_id', for example). + `before`: Only get data that occurs more recently than index + `after`: Only get data that occurs further in the past than index + `limit`: Set amount of data per HTTP response. Default (and + maximum) of 100. + Args: endpoint (str): Endpoint (to be added to base URL) params (Optional[dict]): HTTP request parameters From 287b4deb81217660551dfc11eb9b923c4beefc56 Mon Sep 17 00:00:00 2001 From: ClockeNessMnstr Date: Sun, 9 Jul 2017 15:13:10 -0400 Subject: [PATCH 057/174] self.stop reset issue This line caused a hangup where the main script attempted to exit but the thread hung. on_close() ran but self.stop was reset and the thread hung in _listen(). --- gdax/websocket_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index 0352dacc..6e16f5b2 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -40,7 +40,6 @@ def _connect(self): self.ws = create_connection(self.url) - self.stop = False sub_params = {'type': 'subscribe', 'product_ids': self.products} self.ws.send(json.dumps(sub_params)) if self.type == "heartbeat": From b24c1281be3327a8b9f04bb232c90df50c1f1129 Mon Sep 17 00:00:00 2001 From: Aleks Pesti Date: Thu, 13 Jul 2017 23:53:06 -0400 Subject: [PATCH 058/174] fixed market order change --- gdax/order_book.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gdax/order_book.py b/gdax/order_book.py index b29caf30..c62640e4 100644 --- a/gdax/order_book.py +++ b/gdax/order_book.py @@ -156,7 +156,11 @@ def match(self, order): self.set_asks(price, asks) def change(self, order): - new_size = Decimal(order['new_size']) + try: + new_size = Decimal(order['new_size']) + except KeyError: + return + price = Decimal(order['price']) if order['side'] == 'buy': From 1e8004afd6e08d498cce47114c28c2828ee7ad05 Mon Sep 17 00:00:00 2001 From: Dan Paquin Date: Fri, 14 Jul 2017 22:02:18 -0400 Subject: [PATCH 059/174] Added authentication functionality for websocket --- .gitignore | 2 ++ README.md | 2 +- gdax/authenticated_client.py | 1 + gdax/websocket_client.py | 32 ++++++++++++++++++++++++++++---- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index ad1bd98b..cb98d5cc 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ dist/ *.egg-info/ *.rst venv/ +*log.txt +gdax/__pycache__/ diff --git a/README.md b/README.md index 6ec6251a..ff64e668 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# GDAX-Python +# gdax-python The Python client for the [GDAX API](https://docs.gdax.com/) (formerly known as the Coinbase Exchange API) diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index ec9df0c4..6c5db357 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -294,6 +294,7 @@ def __init__(self, api_key, secret_key, passphrase): def __call__(self, request): timestamp = str(time.time()) message = timestamp + request.method + request.path_url + (request.body or '') + print message message = message.encode('ascii') hmac_key = base64.b64decode(self.secret_key) signature = hmac.new(hmac_key, message, hashlib.sha256) diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index 6e16f5b2..247fa6e1 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -6,19 +6,31 @@ from __future__ import print_function import json - +import base64 +import hmac +import hashlib +import time from threading import Thread from websocket import create_connection, WebSocketConnectionClosedException class WebsocketClient(object): - def __init__(self, url="wss://ws-feed.gdax.com", products=None, message_type="subscribe"): + def __init__(self, url="wss://ws-feed.gdax.com", products=None, message_type="subscribe", auth=False, api_key="", api_secret="", api_passphrase=""): self.url = url self.products = products self.type = message_type self.stop = False self.ws = None self.thread = None + self.auth = auth + self.api_key = api_key + self.api_secret = api_secret + self.api_passphrase = api_passphrase + + def _auth_message(self, method, path, options=None): + + + return auth def start(self): def _go(): @@ -38,10 +50,22 @@ def _connect(self): if self.url[-1] == "/": self.url = self.url[:-1] - self.ws = create_connection(self.url) - sub_params = {'type': 'subscribe', 'product_ids': self.products} + if self.auth: + timestamp = str(time.time()) + message = timestamp + 'GET' + '/users/self' + message = message.encode('ascii') + hmac_key = base64.b64decode(self.api_secret) + signature = hmac.new(hmac_key, message, hashlib.sha256) + signature_b64 = base64.b64encode(signature.digest()) + sub_params['signature'] = signature_b64 + sub_params['key'] = self.api_key + sub_params['passphrase'] = self.api_passphrase + sub_params['timestamp'] = timestamp + + self.ws = create_connection(self.url) self.ws.send(json.dumps(sub_params)) + if self.type == "heartbeat": sub_params = {"type": "heartbeat", "on": True} self.ws.send(json.dumps(sub_params)) From b45f9f7544b87696c8e89ee37c8dea5e14f1fbd1 Mon Sep 17 00:00:00 2001 From: Vit Listik Date: Sat, 15 Jul 2017 22:19:13 +0200 Subject: [PATCH 060/174] python3 compatible print --- gdax/authenticated_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index 6c5db357..cea923ba 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -294,7 +294,7 @@ def __init__(self, api_key, secret_key, passphrase): def __call__(self, request): timestamp = str(time.time()) message = timestamp + request.method + request.path_url + (request.body or '') - print message + print(message) message = message.encode('ascii') hmac_key = base64.b64decode(self.secret_key) signature = hmac.new(hmac_key, message, hashlib.sha256) From 26ba49345e3fe2887c58e9d3f3d457d7f36f843f Mon Sep 17 00:00:00 2001 From: Dan Paquin Date: Mon, 17 Jul 2017 22:19:38 -0400 Subject: [PATCH 061/174] Removed print from authenticated_client --- gdax/authenticated_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index cea923ba..ec9df0c4 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -294,7 +294,6 @@ def __init__(self, api_key, secret_key, passphrase): def __call__(self, request): timestamp = str(time.time()) message = timestamp + request.method + request.path_url + (request.body or '') - print(message) message = message.encode('ascii') hmac_key = base64.b64decode(self.secret_key) signature = hmac.new(hmac_key, message, hashlib.sha256) From d2f8831366e7165c3a32a67498d06da46fe2737e Mon Sep 17 00:00:00 2001 From: Dan Paquin Date: Wed, 19 Jul 2017 20:56:14 -0400 Subject: [PATCH 062/174] Added websocket pinger to keep connection alive --- gdax/websocket_client.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index 247fa6e1..f283d0c1 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -27,11 +27,6 @@ def __init__(self, url="wss://ws-feed.gdax.com", products=None, message_type="su self.api_secret = api_secret self.api_passphrase = api_passphrase - def _auth_message(self, method, path, options=None): - - - return auth - def start(self): def _go(): self._connect() @@ -73,6 +68,9 @@ def _connect(self): def _listen(self): while not self.stop: try: + if int(time.time() % 30) == 0: + # Set a 30 second ping to keep connection alive + self.ws.ping("keepalive") msg = json.loads(self.ws.recv()) except Exception as e: self.on_error(e) @@ -101,7 +99,7 @@ def on_message(self, msg): print(msg) def on_error(self, e): - return + print(e) if __name__ == "__main__": import gdax @@ -126,7 +124,7 @@ def on_close(self): wsClient.start() print(wsClient.url, wsClient.products) # Do some logic with the data - while wsClient.message_count < 500: + while wsClient.message_count < 10000: print("\nMessageCount =", "%i \n" % wsClient.message_count) time.sleep(1) From f7eab7bc72aa9eed3da2e7c0214e384a6234dd6b Mon Sep 17 00:00:00 2001 From: Jiang Bian Date: Sat, 22 Jul 2017 09:12:06 -0400 Subject: [PATCH 063/174] wait the thread ends before close the ws, otherwise, it causes seg fault --- gdax/websocket_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index f283d0c1..b1819611 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -83,6 +83,7 @@ def close(self): self.ws.send(json.dumps({"type": "heartbeat", "on": False})) self.on_close() self.stop = True + self.thread.join() try: if self.ws: self.ws.close() From d3e702764864a698ca37f57a70e016a0adf31837 Mon Sep 17 00:00:00 2001 From: Tobias Date: Sun, 23 Jul 2017 17:41:46 -0400 Subject: [PATCH 064/174] Correct minor typos --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ff64e668..0766d34b 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ what and who is *really* behind every tick. ## Getting Started This README is documentation on the syntax of the python client presented in this repository. See function docstrings for full syntax details. -**This API attempts to present a clean interface to GDAX, but n order to use it +**This API attempts to present a clean interface to GDAX, but in order to use it to its full potential, you must familiarize yourself with the official GDAX documentation.** @@ -321,7 +321,7 @@ workflow. *0.1.2* - Updated JSON handling for increased compatibility among some users. -- Added support for payment methods, reports, and coinbase user accounts. +- Added support for payment methods, reports, and Coinbase user accounts. - Other compatibility updates. *0.1.1b2* From b65ed87619815e52be28650150b1217d7c65ff08 Mon Sep 17 00:00:00 2001 From: Mike Cardillo Date: Thu, 27 Jul 2017 01:49:37 -0700 Subject: [PATCH 065/174] Reset WebsocketClient.stop to False when calling .start() WebsocketClient.stop is set to True when .close() is called, but it is never set back to False when start() is called, so _listen() never runs. --- gdax/websocket_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index f283d0c1..dd809856 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -32,6 +32,7 @@ def _go(): self._connect() self._listen() + self.stop = False self.on_open() self.thread = Thread(target=_go) self.thread.start() From 62566c9335dec8c3a3b17c3f020e965dca875779 Mon Sep 17 00:00:00 2001 From: Mike Cardillo Date: Thu, 27 Jul 2017 02:07:55 -0700 Subject: [PATCH 066/174] Moved heartbeat disable command to connect() Fixes #103. Sending the heartbeat disable command in close() was causing a WebSocketConnectionClosedException if websocket was already closed. --- gdax/websocket_client.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index f283d0c1..3bf7b5b1 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -63,7 +63,10 @@ def _connect(self): if self.type == "heartbeat": sub_params = {"type": "heartbeat", "on": True} - self.ws.send(json.dumps(sub_params)) + else: + sub_params = {"type": "heartbeat", "on": False} + self.ws.send(json.dumps(sub_params)) + def _listen(self): while not self.stop: @@ -79,8 +82,6 @@ def _listen(self): def close(self): if not self.stop: - if self.type == "heartbeat": - self.ws.send(json.dumps({"type": "heartbeat", "on": False})) self.on_close() self.stop = True try: From 4709936d123e915998d3afaf0ffd0cf7a3e0af60 Mon Sep 17 00:00:00 2001 From: Mike Cardillo Date: Fri, 28 Jul 2017 15:40:48 -0700 Subject: [PATCH 067/174] Re-initialize OrderBook on open If OrderBook is closed, then reopened, the accuracy of the data can no longer be trusted. Setting _sequence = -1 on open resets the _asks and _bids members as well so we can start with a fresh and accurate OrderBook. --- gdax/order_book.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/gdax/order_book.py b/gdax/order_book.py index c62640e4..924c62fa 100644 --- a/gdax/order_book.py +++ b/gdax/order_book.py @@ -29,6 +29,13 @@ def product_id(self): ''' Currently OrderBook only supports a single product even though it is stored as a list of products. ''' return self.products[0] + def on_open(self): + self._sequence = -1 + print("-- Subscribed to OrderBook! --\n") + + def on_close(self): + print("\n-- OrderBook Socket Closed! --") + def on_message(self, message): if self._log_to: pickle.dump(message, self._log_to) From e44a41513204e07a33661fd237bb187c6e75baa0 Mon Sep 17 00:00:00 2001 From: Dan Paquin Date: Sun, 30 Jul 2017 21:36:42 -0400 Subject: [PATCH 068/174] added timeout to authenticatedclient and publicclient, add product_id attributes to cancel_all and get_orders --- gdax/authenticated_client.py | 85 +++++++++++++++++++----------------- gdax/public_client.py | 16 +++---- 2 files changed, 54 insertions(+), 47 deletions(-) diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index ec9df0c4..174b262d 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -20,7 +20,7 @@ def __init__(self, key, b64secret, passphrase, api_url="https://api.gdax.com"): self.auth = GdaxAuth(key, b64secret, passphrase) def get_account(self, account_id): - r = requests.get(self.url + '/accounts/' + account_id, auth=self.auth) + r = requests.get(self.url + '/accounts/' + account_id, auth=self.auth, timeout=30) # r.raise_for_status() return r.json() @@ -29,7 +29,7 @@ def get_accounts(self): def get_account_history(self, account_id): result = [] - r = requests.get(self.url + '/accounts/{}/ledger'.format(account_id), auth=self.auth) + r = requests.get(self.url + '/accounts/{}/ledger'.format(account_id), auth=self.auth, timeout=30) # r.raise_for_status() result.append(r.json()) if "cb-after" in r.headers: @@ -37,7 +37,7 @@ def get_account_history(self, account_id): return result def history_pagination(self, account_id, result, after): - r = requests.get(self.url + '/accounts/{}/ledger?after={}'.format(account_id, str(after)), auth=self.auth) + r = requests.get(self.url + '/accounts/{}/ledger?after={}'.format(account_id, str(after)), auth=self.auth, timeout=30) # r.raise_for_status() if r.json(): result.append(r.json()) @@ -47,7 +47,7 @@ def history_pagination(self, account_id, result, after): def get_account_holds(self, account_id): result = [] - r = requests.get(self.url + '/accounts/{}/holds'.format(account_id), auth=self.auth) + r = requests.get(self.url + '/accounts/{}/holds'.format(account_id), auth=self.auth, timeout=30) # r.raise_for_status() result.append(r.json()) if "cb-after" in r.headers: @@ -55,7 +55,7 @@ def get_account_holds(self, account_id): return result def holds_pagination(self, account_id, result, after): - r = requests.get(self.url + '/accounts/{}/holds?after={}'.format(account_id, str(after)), auth=self.auth) + r = requests.get(self.url + '/accounts/{}/holds?after={}'.format(account_id, str(after)), auth=self.auth, timeout=30) # r.raise_for_status() if r.json(): result.append(r.json()) @@ -69,46 +69,53 @@ def buy(self, **kwargs): kwargs["product_id"] = self.product_id r = requests.post(self.url + '/orders', data=json.dumps(kwargs), - auth=self.auth) + auth=self.auth, + timeout=30) return r.json() def sell(self, **kwargs): kwargs["side"] = "sell" r = requests.post(self.url + '/orders', data=json.dumps(kwargs), - auth=self.auth) + auth=self.auth, + timeout=30) return r.json() def cancel_order(self, order_id): - r = requests.delete(self.url + '/orders/' + order_id, auth=self.auth) + r = requests.delete(self.url + '/orders/' + order_id, auth=self.auth, timeout=30) # r.raise_for_status() return r.json() - def cancel_all(self, data=None, product=''): - if type(data) is dict: - if "product" in data: - product = data["product"] - r = requests.delete(self.url + '/orders/', - data=json.dumps({'product_id': product or self.product_id}), auth=self.auth) + def cancel_all(self, product_id=''): + url = self.url + '/orders/' + if product_id: + url += "?product_id={}&".format(str(product_id)) + r = requests.delete(url, auth=self.auth, timeout=30) # r.raise_for_status() return r.json() def get_order(self, order_id): - r = requests.get(self.url + '/orders/' + order_id, auth=self.auth) + r = requests.get(self.url + '/orders/' + order_id, auth=self.auth, timeout=30) # r.raise_for_status() return r.json() - def get_orders(self): + def get_orders(self, product_id=''): result = [] - r = requests.get(self.url + '/orders/', auth=self.auth) + url = self.url + '/orders/' + if product_id: + url += "?product_id={}&".format(product_id) + r = requests.get(url, auth=self.auth, timeout=30) # r.raise_for_status() result.append(r.json()) if 'cb-after' in r.headers: - self.paginate_orders(result, r.headers['cb-after']) + self.paginate_orders(product_id, result, r.headers['cb-after']) return result - def paginate_orders(self, result, after): - r = requests.get(self.url + '/orders?after={}'.format(str(after)), auth=self.auth) + def paginate_orders(self, product_id, result, after): + url = self.url + '/orders?after={}&'.format(str(after)) + if product_id: + url += "product_id={}&".format(product_id) + r = requests.get(url, auth=self.auth, timeout=30) # r.raise_for_status() if r.json(): result.append(r.json()) @@ -122,14 +129,14 @@ def get_fills(self, order_id='', product_id='', before='', after='', limit=''): if order_id: url += "order_id={}&".format(str(order_id)) if product_id: - url += "product_id={}&".format(product_id or self.product_id) + url += "product_id={}&".format(product_id) if before: url += "before={}&".format(str(before)) if after: url += "after={}&".format(str(after)) if limit: url += "limit={}&".format(str(limit)) - r = requests.get(url, auth=self.auth) + r = requests.get(url, auth=self.auth, timeout=30) # r.raise_for_status() result.append(r.json()) if 'cb-after' in r.headers and limit is not len(r.json()): @@ -141,8 +148,8 @@ def paginate_fills(self, result, after, order_id='', product_id=''): if order_id: url += "order_id={}&".format(str(order_id)) if product_id: - url += "product_id={}&".format(product_id or self.product_id) - r = requests.get(url, auth=self.auth) + url += "product_id={}&".format(product_id) + r = requests.get(url, auth=self.auth, timeout=30) # r.raise_for_status() if r.json(): result.append(r.json()) @@ -158,7 +165,7 @@ def get_fundings(self, result='', status='', after=''): url += "status={}&".format(str(status)) if after: url += 'after={}&'.format(str(after)) - r = requests.get(url, auth=self.auth) + r = requests.get(url, auth=self.auth, timeout=30) # r.raise_for_status() result.append(r.json()) if 'cb-after' in r.headers: @@ -170,7 +177,7 @@ def repay_funding(self, amount='', currency=''): "amount": amount, "currency": currency # example: USD } - r = requests.post(self.url + "/funding/repay", data=json.dumps(payload), auth=self.auth) + r = requests.post(self.url + "/funding/repay", data=json.dumps(payload), auth=self.auth, timeout=30) # r.raise_for_status() return r.json() @@ -181,12 +188,12 @@ def margin_transfer(self, margin_profile_id="", transfer_type="", currency="", a "currency": currency, # example: USD "amount": amount } - r = requests.post(self.url + "/profiles/margin-transfer", data=json.dumps(payload), auth=self.auth) + r = requests.post(self.url + "/profiles/margin-transfer", data=json.dumps(payload), auth=self.auth, timeout=30) # r.raise_for_status() return r.json() def get_position(self): - r = requests.get(self.url + "/position", auth=self.auth) + r = requests.get(self.url + "/position", auth=self.auth, timeout=30) # r.raise_for_status() return r.json() @@ -194,7 +201,7 @@ def close_position(self, repay_only=""): payload = { "repay_only": repay_only or False } - r = requests.post(self.url + "/position/close", data=json.dumps(payload), auth=self.auth) + r = requests.post(self.url + "/position/close", data=json.dumps(payload), auth=self.auth, timeout=30) # r.raise_for_status() return r.json() @@ -204,7 +211,7 @@ def deposit(self, amount="", currency="", payment_method_id=""): "currency": currency, "payment_method_id": payment_method_id } - r = requests.post(self.url + "/deposits/payment-method", data=json.dumps(payload), auth=self.auth) + r = requests.post(self.url + "/deposits/payment-method", data=json.dumps(payload), auth=self.auth, timeout=30) # r.raise_for_status() return r.json() @@ -214,7 +221,7 @@ def coinbase_deposit(self, amount="", currency="", coinbase_account_id=""): "currency": currency, "coinbase_account_id": coinbase_account_id } - r = requests.post(self.url + "/deposits/coinbase-account", data=json.dumps(payload), auth=self.auth) + r = requests.post(self.url + "/deposits/coinbase-account", data=json.dumps(payload), auth=self.auth, timeout=30) # r.raise_for_status() return r.json() @@ -224,7 +231,7 @@ def withdraw(self, amount="", currency="", payment_method_id=""): "currency": currency, "payment_method_id": payment_method_id } - r = requests.post(self.url + "/withdrawals/payment-method", data=json.dumps(payload), auth=self.auth) + r = requests.post(self.url + "/withdrawals/payment-method", data=json.dumps(payload), auth=self.auth, timeout=30) # r.raise_for_status() return r.json() @@ -234,7 +241,7 @@ def coinbase_withdraw(self, amount="", currency="", coinbase_account_id=""): "currency": currency, "coinbase_account_id": coinbase_account_id } - r = requests.post(self.url + "/withdrawals/coinbase", data=json.dumps(payload), auth=self.auth) + r = requests.post(self.url + "/withdrawals/coinbase", data=json.dumps(payload), auth=self.auth, timeout=30) # r.raise_for_status() return r.json() @@ -244,17 +251,17 @@ def crypto_withdraw(self, amount="", currency="", crypto_address=""): "currency": currency, "crypto_address": crypto_address } - r = requests.post(self.url + "/withdrawals/crypto", data=json.dumps(payload), auth=self.auth) + r = requests.post(self.url + "/withdrawals/crypto", data=json.dumps(payload), auth=self.auth, timeout=30) # r.raise_for_status() return r.json() def get_payment_methods(self): - r = requests.get(self.url + "/payment-methods", auth=self.auth) + r = requests.get(self.url + "/payment-methods", auth=self.auth, timeout=30) # r.raise_for_status() return r.json() def get_coinbase_accounts(self): - r = requests.get(self.url + "/coinbase-accounts", auth=self.auth) + r = requests.get(self.url + "/coinbase-accounts", auth=self.auth, timeout=30) # r.raise_for_status() return r.json() @@ -269,17 +276,17 @@ def create_report(self, report_type="", start_date="", end_date="", product_id=" "format": report_format, "email": email } - r = requests.post(self.url + "/reports", data=json.dumps(payload), auth=self.auth) + r = requests.post(self.url + "/reports", data=json.dumps(payload), auth=self.auth, timeout=30) # r.raise_for_status() return r.json() def get_report(self, report_id=""): - r = requests.get(self.url + "/reports/" + report_id, auth=self.auth) + r = requests.get(self.url + "/reports/" + report_id, auth=self.auth, timeout=30) # r.raise_for_status() return r.json() def get_trailing_volume(self): - r = requests.get(self.url + "/users/self/trailing-volume", auth=self.auth) + r = requests.get(self.url + "/users/self/trailing-volume", auth=self.auth, timeout=30) # r.raise_for_status() return r.json() diff --git a/gdax/public_client.py b/gdax/public_client.py index ecbc7880..91d4f5cf 100644 --- a/gdax/public_client.py +++ b/gdax/public_client.py @@ -45,7 +45,7 @@ def get_products(self): ] """ - r = requests.get(self.url + '/products') + r = requests.get(self.url + '/products', timeout=30) # r.raise_for_status() return r.json() @@ -86,7 +86,7 @@ def get_product_order_book(self, product_id, level=1): """ params = {'level': level} r = requests.get(self.url + '/products/{}/book' - .format(product_id), params=params) + .format(product_id), params=params, timeout=30) # r.raise_for_status() return r.json() @@ -113,7 +113,7 @@ def get_product_ticker(self, product_id): """ r = requests.get(self.url + '/products/{}/ticker' - .format(product_id)) + .format(product_id), timeout=30) # r.raise_for_status() return r.json() @@ -140,7 +140,7 @@ def get_product_trades(self, product_id): }] """ - r = requests.get(self.url + '/products/{}/trades'.format(product_id)) + r = requests.get(self.url + '/products/{}/trades'.format(product_id), timeout=30) # r.raise_for_status() return r.json() @@ -189,7 +189,7 @@ def get_product_historic_rates(self, product_id, start=None, end=None, if granularity is not None: params['granularity'] = granularity r = requests.get(self.url + '/products/{}/candles' - .format(product_id), params=params) + .format(product_id), params=params, timeout=30) # r.raise_for_status() return r.json() @@ -210,7 +210,7 @@ def get_product_24hr_stats(self, product_id): } """ - r = requests.get(self.url + '/products/{}/stats'.format(product_id)) + r = requests.get(self.url + '/products/{}/stats'.format(product_id), timeout=30) # r.raise_for_status() return r.json() @@ -230,7 +230,7 @@ def get_currencies(self): }] """ - r = requests.get(self.url + '/currencies') + r = requests.get(self.url + '/currencies', timeout=30) # r.raise_for_status() return r.json() @@ -246,6 +246,6 @@ def get_time(self): } """ - r = requests.get(self.url + '/time') + r = requests.get(self.url + '/time', timeout=30) # r.raise_for_status() return r.json() From b37e558ad9bdd5435a5c9f7d0f1902bf25260d7f Mon Sep 17 00:00:00 2001 From: Aleks Pesti Date: Wed, 2 Aug 2017 00:03:42 -0400 Subject: [PATCH 069/174] fixes crash on update market sell --- gdax/order_book.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gdax/order_book.py b/gdax/order_book.py index c62640e4..a2627389 100644 --- a/gdax/order_book.py +++ b/gdax/order_book.py @@ -160,8 +160,11 @@ def change(self, order): new_size = Decimal(order['new_size']) except KeyError: return - - price = Decimal(order['price']) + + try: + price = Decimal(order['price']) + except KeyError: + return if order['side'] == 'buy': bids = self.get_bids(price) From 4feb6a1f0056b86ec7ba8d2dbc4c43bd3d2295bc Mon Sep 17 00:00:00 2001 From: Manu NALEPA Date: Sat, 16 Sep 2017 00:58:59 +0200 Subject: [PATCH 070/174] Fix typo in README.txt "n order to" ==> "in order to" --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ff64e668..1aa8bf73 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ what and who is *really* behind every tick. ## Getting Started This README is documentation on the syntax of the python client presented in this repository. See function docstrings for full syntax details. -**This API attempts to present a clean interface to GDAX, but n order to use it +**This API attempts to present a clean interface to GDAX, but in order to use it to its full potential, you must familiarize yourself with the official GDAX documentation.** From 5134cd4b506da2df706c67fef02d5d017f06da10 Mon Sep 17 00:00:00 2001 From: Dominik Piekarczyk Date: Sun, 17 Sep 2017 12:26:13 +0200 Subject: [PATCH 071/174] example print formatting error fixed --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ff64e668..bb9dbb2a 100644 --- a/README.md +++ b/README.md @@ -263,7 +263,7 @@ class myWebsocketClient(gdax.WebsocketClient): self.message_count += 1 if 'price' in msg and 'type' in msg: print ("Message type:", msg["type"], - "\t@ {}.3f".format(float(msg["price"]))) + "\t@ {:.3f}".format(float(msg["price"]))) def on_close(self): print("-- Goodbye! --") From 5c9bc2b375a4687e3e6767cc9cb41ff0f94f4d62 Mon Sep 17 00:00:00 2001 From: acontry Date: Thu, 21 Sep 2017 23:32:37 -0700 Subject: [PATCH 072/174] Add back buy and sell methods --- gdax/authenticated_client.py | 44 ++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index 57334c8e..f315bd44 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -254,6 +254,46 @@ def place_order(self, product_id, side, order_type, **kwargs): params.update(kwargs) return self._send_message('post', '/orders', data=json.dumps(params)) + def buy(self, product_id, order_type, **kwargs): + """Place a buy order. + + This is included to maintain backwards compatibility with older versions + of GDAX-Python. For maximum support from docstrings and function + signatures see the order type-specific functions place_limit_order, + place_market_order, and place_stop_order. + + Args: + product_id (str): Product to order (eg. 'BTC-USD') + order_type (str): Order type ('limit', 'market', or 'stop') + **kwargs: Additional arguments can be specified for different order + types. + + Returns: + dict: Order details. See `place_order` for example. + + """ + return self.place_order(product_id, 'buy', order_type, **kwargs) + + def sell(self, product_id, order_type, **kwargs): + """Place a sell order. + + This is included to maintain backwards compatibility with older versions + of GDAX-Python. For maximum support from docstrings and function + signatures see the order type-specific functions place_limit_order, + place_market_order, and place_stop_order. + + Args: + product_id (str): Product to order (eg. 'BTC-USD') + order_type (str): Order type ('limit', 'market', or 'stop') + **kwargs: Additional arguments can be specified for different order + types. + + Returns: + dict: Order details. See `place_order` for example. + + """ + return self.place_order(product_id, 'sell', order_type, **kwargs) + def place_limit_order(self, product_id, side, price, size, client_oid=None, stp=None, @@ -931,7 +971,7 @@ def get_report(self, report_id): def get_trailing_volume(self): """ Get your 30-day trailing volume for all products. - This is a cached value that’s calculated every day at midnight UTC. + This is a cached value that's calculated every day at midnight UTC. Returns: list: 30-day trailing volumes. Example:: @@ -1006,7 +1046,7 @@ def _send_paginated_message(self, endpoint, params=None): break else: params['after'] = r.headers['cb-after'] - + class GdaxAuth(AuthBase): # Provided by gdax: https://docs.gdax.com/#signing-a-message From 5d1c7d122c3d9bdfde6ddf46b210a2cfae67df94 Mon Sep 17 00:00:00 2001 From: acontry Date: Fri, 22 Sep 2017 00:10:26 -0700 Subject: [PATCH 073/174] Move send_message wrappers to public client This move was made so get_product_trades can return a generator like all other paginated API endpoints. --- README.md | 26 ++++++++- gdax/authenticated_client.py | 57 ------------------- gdax/public_client.py | 103 ++++++++++++++++++++++++++--------- tests/test_public_client.py | 3 +- 4 files changed, 104 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index eccc72a0..495f5340 100644 --- a/README.md +++ b/README.md @@ -69,9 +69,10 @@ public_client.get_product_order_book('BTC-USD', level=1) public_client.get_product_ticker(product_id='ETH-USD') ``` -- [get_product_trades](https://docs.gdax.com/#get-trades) +- [get_product_trades](https://docs.gdax.com/#get-trades) (paginated) ```python # Get the product trades for a specific product. +# Returns a generator public_client.get_product_trades(product_id='ETH-USD') ``` @@ -169,14 +170,37 @@ auth_client.get_account_holds("7d0f7d8e-dd34-4d9c-a846-06f431c381ba") # Buy 0.01 BTC @ 100 USD auth_client.buy(price='100.00', #USD size='0.01', #BTC + order_type='limit', product_id='BTC-USD') ``` ```python # Sell 0.01 BTC @ 200 USD auth_client.sell(price='200.00', #USD size='0.01', #BTC + order_type='limit', product_id='BTC-USD') ``` +```python +# Limit order-specific method +auth_client.place_limit_order(product_id='BTC-USD', + side='buy', + price='200.00', + size='0.01') +``` +```python +# Place a market order by specifying amount of USD to use. +# Alternatively, `size` could be used to specify quantity in BTC amount. +auth_client.place_market_order(product_id='BTC-USD', + side='buy', + funds='100.00') +``` +```python +# Stop order. `funds` can be used instead of `size` here. +auth_client.place_stop_order(product_id='BTC-USD', + side='buy', + price='200.00', + size='0.01') +``` - [cancel_order](https://docs.gdax.com/#cancel-an-order) ```python diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index f315bd44..74bab8aa 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -990,63 +990,6 @@ def get_trailing_volume(self): """ return self._send_message('get', '/users/self/trailing-volume') - def _send_message(self, method, endpoint, params=None, data=None): - """Send API request. - - Args: - method (str): HTTP method (get, post, delete, etc.) - endpoint (str): Endpoint (to be added to base URL) - params (Optional[dict]): HTTP request parameters - data (Optional[str]): JSON-encoded string payload for POST - - Returns: - dict/list: JSON response - - """ - url = self.url + endpoint - r = self.session.request(method, url, params=params, data=data, - auth=self.auth, timeout=30) - return r.json() - - def _send_paginated_message(self, endpoint, params=None): - """ Send API message that results in a paginated response. - - The paginated responses are abstracted away by making API requests on - demand as the response is iterated over. - - Paginated API messages support 3 additional parameters: `before`, - `after`, and `limit`. `before` and `after` are mutually exclusive. To - use them, supply an index value for that endpoint (the field used for - indexing varies by endpoint - get_fills() uses 'trade_id', for example). - `before`: Only get data that occurs more recently than index - `after`: Only get data that occurs further in the past than index - `limit`: Set amount of data per HTTP response. Default (and - maximum) of 100. - - Args: - endpoint (str): Endpoint (to be added to base URL) - params (Optional[dict]): HTTP request parameters - - Yields: - dict: API response objects - - """ - url = self.url + endpoint - while True: - r = self.session.get(url, params=params, auth=self.auth, timeout=30) - results = r.json() - for result in results: - yield result - # If there are no more pages, we're done. Otherwise update `after` - # param to get next page. - # If this request included `before` don't get any more pages - the - # GDAX API doesn't support multiple pages in that case. - if not r.headers.get('cb-after') or \ - params.get('before') is not None: - break - else: - params['after'] = r.headers['cb-after'] - class GdaxAuth(AuthBase): # Provided by gdax: https://docs.gdax.com/#signing-a-message diff --git a/gdax/public_client.py b/gdax/public_client.py index 91d4f5cf..21d6d3e1 100644 --- a/gdax/public_client.py +++ b/gdax/public_client.py @@ -26,6 +26,8 @@ def __init__(self, api_url='https://api.gdax.com'): """ self.url = api_url.rstrip('/') + self.auth = None + self.session = requests.Session() def get_products(self): """Get a list of available currency pairs for trading. @@ -45,9 +47,7 @@ def get_products(self): ] """ - r = requests.get(self.url + '/products', timeout=30) - # r.raise_for_status() - return r.json() + return self._send_message('get', '/products') def get_product_order_book(self, product_id, level=1): """Get a list of open orders for a product. @@ -85,10 +85,9 @@ def get_product_order_book(self, product_id, level=1): """ params = {'level': level} - r = requests.get(self.url + '/products/{}/book' - .format(product_id), params=params, timeout=30) - # r.raise_for_status() - return r.json() + return self._send_message('get', + '/products/{}/book'.format(product_id), + params=params) def get_product_ticker(self, product_id): """Snapshot about the last trade (tick), best bid/ask and 24h volume. @@ -112,14 +111,15 @@ def get_product_ticker(self, product_id): } """ - r = requests.get(self.url + '/products/{}/ticker' - .format(product_id), timeout=30) - # r.raise_for_status() - return r.json() + return self._send_message('get', + '/products/{}/ticker'.format(product_id)) def get_product_trades(self, product_id): """List the latest trades for a product. + This method returns a generator which may make multiple HTTP requests + while iterating through it. + Args: product_id (str): Product @@ -140,9 +140,8 @@ def get_product_trades(self, product_id): }] """ - r = requests.get(self.url + '/products/{}/trades'.format(product_id), timeout=30) - # r.raise_for_status() - return r.json() + return self._send_paginated_message('/products/{}/trades' + .format(product_id)) def get_product_historic_rates(self, product_id, start=None, end=None, granularity=None): @@ -188,10 +187,8 @@ def get_product_historic_rates(self, product_id, start=None, end=None, params['end'] = end if granularity is not None: params['granularity'] = granularity - r = requests.get(self.url + '/products/{}/candles' - .format(product_id), params=params, timeout=30) - # r.raise_for_status() - return r.json() + return self._send_message('get', + '/products/{}/candles'.format(product_id)) def get_product_24hr_stats(self, product_id): """Get 24 hr stats for the product. @@ -210,9 +207,8 @@ def get_product_24hr_stats(self, product_id): } """ - r = requests.get(self.url + '/products/{}/stats'.format(product_id), timeout=30) - # r.raise_for_status() - return r.json() + return self._send_message('get', + '/products/{}/stats'.format(product_id)) def get_currencies(self): """List known currencies. @@ -230,9 +226,7 @@ def get_currencies(self): }] """ - r = requests.get(self.url + '/currencies', timeout=30) - # r.raise_for_status() - return r.json() + return self._send_message('get', '/currencies') def get_time(self): """Get the API server time. @@ -246,6 +240,63 @@ def get_time(self): } """ - r = requests.get(self.url + '/time', timeout=30) - # r.raise_for_status() + return self._send_message('get', '/time') + + def _send_message(self, method, endpoint, params=None, data=None): + """Send API request. + + Args: + method (str): HTTP method (get, post, delete, etc.) + endpoint (str): Endpoint (to be added to base URL) + params (Optional[dict]): HTTP request parameters + data (Optional[str]): JSON-encoded string payload for POST + + Returns: + dict/list: JSON response + + """ + url = self.url + endpoint + r = self.session.request(method, url, params=params, data=data, + auth=self.auth, timeout=30) return r.json() + + def _send_paginated_message(self, endpoint, params=None): + """ Send API message that results in a paginated response. + + The paginated responses are abstracted away by making API requests on + demand as the response is iterated over. + + Paginated API messages support 3 additional parameters: `before`, + `after`, and `limit`. `before` and `after` are mutually exclusive. To + use them, supply an index value for that endpoint (the field used for + indexing varies by endpoint - get_fills() uses 'trade_id', for example). + `before`: Only get data that occurs more recently than index + `after`: Only get data that occurs further in the past than index + `limit`: Set amount of data per HTTP response. Default (and + maximum) of 100. + + Args: + endpoint (str): Endpoint (to be added to base URL) + params (Optional[dict]): HTTP request parameters + + Yields: + dict: API response objects + + """ + if params is None: + params = dict() + url = self.url + endpoint + while True: + r = self.session.get(url, params=params, auth=self.auth, timeout=30) + results = r.json() + for result in results: + yield result + # If there are no more pages, we're done. Otherwise update `after` + # param to get next page. + # If this request included `before` don't get any more pages - the + # GDAX API doesn't support multiple pages in that case. + if not r.headers.get('cb-after') or \ + params.get('before') is not None: + break + else: + params['after'] = r.headers['cb-after'] diff --git a/tests/test_public_client.py b/tests/test_public_client.py index 5da77c27..45f7ed5b 100644 --- a/tests/test_public_client.py +++ b/tests/test_public_client.py @@ -1,4 +1,5 @@ import pytest +from itertools import islice import gdax @@ -28,7 +29,7 @@ def test_get_product_ticker(self, client): assert 'trade_id' in r def test_get_product_trades(self, client): - r = client.get_product_trades('BTC-USD') + r = list(islice(client.get_product_trades('BTC-USD'), 200)) assert type(r) is list assert 'trade_id' in r[0] From ac116b335a7ca90dbab4a1215da4ee8c0d4327aa Mon Sep 17 00:00:00 2001 From: matt Date: Thu, 28 Sep 2017 09:35:07 -0600 Subject: [PATCH 074/174] Corrected arguments in paginate_orders --- gdax/authenticated_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index 174b262d..6e00e616 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -120,7 +120,7 @@ def paginate_orders(self, product_id, result, after): if r.json(): result.append(r.json()) if 'cb-after' in r.headers: - self.paginate_orders(result, r.headers['cb-after']) + self.paginate_orders(product_id, result, r.headers['cb-after']) return result def get_fills(self, order_id='', product_id='', before='', after='', limit=''): From 7d8568d743778451109e58f2bb17fdffcc97d432 Mon Sep 17 00:00:00 2001 From: Anvita Pandit Date: Tue, 3 Oct 2017 00:13:23 -0400 Subject: [PATCH 075/174] No more default product_id --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 75adb699..a66cb5d3 100644 --- a/README.md +++ b/README.md @@ -110,9 +110,7 @@ integrate both into your script. ```python import gdax auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase) -# Set a default product -auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase, - product_id="ETH-USD") + # Use the sandbox API (requires a different set of API access credentials) auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase, api_url="https://api-public.sandbox.gdax.com") @@ -246,7 +244,7 @@ can react to the data streaming in. The current client is a template used for illustration purposes only. - onOpen - called once, *immediately before* the socket connection is made, this -is where you want to add inital parameters. +is where you want to add initial parameters. - onMessage - called once for every message that arrives and accepts one argument that contains the message of dict type. - onClose - called once after the websocket has been closed. From 71572f544f5972e1f321e4f6f7a65018ad69d941 Mon Sep 17 00:00:00 2001 From: Bud Bach Date: Mon, 25 Sep 2017 13:26:59 -0500 Subject: [PATCH 076/174] Clean up to fix a thead joining issue There were a couple of places where an OrderBook() calls `close()` on itself. Since `OrderBook()` is derived from `WebsocketClient()`, ultimately, the call resolves to `WebsocketClient.close()`. `WebsocketClient.close()` tries to call `join()` on the thread it is currently running on which is not allowed. To solve these to issues, WebsocketClient() was changed such that it closes the web socket on the thread it is created when is calls _disconnect(). An error attribute was also added. If on_error is called, the default action is now to set the thread to stop and set the error attribute value to be the exception that occured. The user can then decide what to when the WebsocketClient() returns with an error being set. This behavior can be overridden by a subclass if desired. The order book now attempts to recover the book after a sequence gap by resetting the book and grabbing a new snapshot, rather than trying to close() and start(). Previously, it was creating a new thread from the old thread which would leave dangling threads. The order book no longer has an on_error() method (since it did basically the same thing as toe old book recovery was doing) and instead relies on the default action described above. --- gdax/order_book.py | 77 +++++++++++++++++++++------------------- gdax/websocket_client.py | 56 ++++++++++++++++++----------- 2 files changed, 76 insertions(+), 57 deletions(-) diff --git a/gdax/order_book.py b/gdax/order_book.py index 259e5c5c..6af28f0c 100644 --- a/gdax/order_book.py +++ b/gdax/order_book.py @@ -36,38 +36,39 @@ def on_open(self): def on_close(self): print("\n-- OrderBook Socket Closed! --") + def reset_book(self): + self._asks = RBTree() + self._bids = RBTree() + res = self._client.get_product_order_book(product_id=self.product_id, level=3) + for bid in res['bids']: + self.add({ + 'id': bid[2], + 'side': 'buy', + 'price': Decimal(bid[0]), + 'size': Decimal(bid[1]) + }) + for ask in res['asks']: + self.add({ + 'id': ask[2], + 'side': 'sell', + 'price': Decimal(ask[0]), + 'size': Decimal(ask[1]) + }) + self._sequence = res['sequence'] + def on_message(self, message): if self._log_to: pickle.dump(message, self._log_to) sequence = message['sequence'] if self._sequence == -1: - self._asks = RBTree() - self._bids = RBTree() - res = self._client.get_product_order_book(product_id=self.product_id, level=3) - for bid in res['bids']: - self.add({ - 'id': bid[2], - 'side': 'buy', - 'price': Decimal(bid[0]), - 'size': Decimal(bid[1]) - }) - for ask in res['asks']: - self.add({ - 'id': ask[2], - 'side': 'sell', - 'price': Decimal(ask[0]), - 'size': Decimal(ask[1]) - }) - self._sequence = res['sequence'] - + self.reset_book() + return if sequence <= self._sequence: # ignore older messages (e.g. before order book initialization from getProductOrderBook) return elif sequence > self._sequence + 1: - print('Error: messages missing ({} - {}). Re-initializing websocket.'.format(sequence, self._sequence)) - self.close() - self.start() + self.on_sequence_gap(self._sequence, sequence) return msg_type = message['type'] @@ -83,18 +84,11 @@ def on_message(self, message): self._sequence = sequence - # bid = self.get_bid() - # bids = self.get_bids(bid) - # bid_depth = sum([b['size'] for b in bids]) - # ask = self.get_ask() - # asks = self.get_asks(ask) - # ask_depth = sum([a['size'] for a in asks]) - # print('bid: %f @ %f - ask: %f @ %f' % (bid_depth, bid, ask_depth, ask)) + def on_sequence_gap(self, gap_start, gap_end): + self.reset_book() + print('Error: messages missing ({} - {}). Re-initializing book at sequence.'.format( + gap_start, gap_end, self._sequence)) - def on_error(self, e): - self._sequence = -1 - self.close() - self.start() def add(self, order): order = { @@ -250,6 +244,7 @@ def set_bids(self, price, bids): if __name__ == '__main__': + import sys import time import datetime as dt @@ -286,10 +281,18 @@ def on_message(self, message): self._ask = ask self._bid_depth = bid_depth self._ask_depth = ask_depth - print('{}\tbid: {:.3f} @ {:.2f}\task: {:.3f} @ {:.2f}'.format(dt.datetime.now(), bid_depth, bid, - ask_depth, ask)) + print('{} {} bid: {:.3f} @ {:.2f}\task: {:.3f} @ {:.2f}'.format( + dt.datetime.now(), self.product_id, bid_depth, bid, ask_depth, ask)) order_book = OrderBookConsole() order_book.start() - time.sleep(10) - order_book.close() + try: + while True: + time.sleep(10) + except KeyboardInterrupt: + order_book.close() + + if order_book.error: + sys.exit(1) + else: + sys.exit(0) diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index ab0f96ff..b8196bf2 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -20,6 +20,7 @@ def __init__(self, url="wss://ws-feed.gdax.com", products=None, message_type="su self.products = products self.type = message_type self.stop = False + self.error = None self.ws = None self.thread = None self.auth = auth @@ -31,6 +32,7 @@ def start(self): def _go(): self._connect() self._listen() + self._disconnect() self.stop = False self.on_open() @@ -75,22 +77,29 @@ def _listen(self): if int(time.time() % 30) == 0: # Set a 30 second ping to keep connection alive self.ws.ping("keepalive") - msg = json.loads(self.ws.recv()) + data = self.ws.recv() + msg = json.loads(data) + except ValueError as e: + self.on_error(e) except Exception as e: self.on_error(e) else: self.on_message(msg) + def _disconnect(self): + if self.type == "heartbeat": + self.ws.send(json.dumps({"type": "heartbeat", "on": False})) + try: + if self.ws: + self.ws.close() + except WebSocketConnectionClosedException as e: + pass + + self.on_close() + def close(self): - if not self.stop: - self.on_close() - self.stop = True - self.thread.join() - try: - if self.ws: - self.ws.close() - except WebSocketConnectionClosedException as e: - pass + self.stop = True + self.thread.join() def on_open(self): print("-- Subscribed! --\n") @@ -101,10 +110,13 @@ def on_close(self): def on_message(self, msg): print(msg) - def on_error(self, e): - print(e) + def on_error(self, e, data=None): + self.error = e + self.stop + print('{} - data: {}'.format(e, data)) if __name__ == "__main__": + import sys import gdax import time @@ -116,8 +128,7 @@ def on_open(self): print("Let's count the messages!") def on_message(self, msg): - if 'price' in msg and 'type' in msg: - print("Message type:", msg["type"], "\t@ %.3f" % float(msg["price"])) + print(json.dumps(msg, indent=4, sort_keys=True)) self.message_count += 1 def on_close(self): @@ -126,9 +137,14 @@ def on_close(self): wsClient = MyWebsocketClient() wsClient.start() print(wsClient.url, wsClient.products) - # Do some logic with the data - while wsClient.message_count < 10000: - print("\nMessageCount =", "%i \n" % wsClient.message_count) - time.sleep(1) - - wsClient.close() + try: + while True: + print("\nMessageCount =", "%i \n" % wsClient.message_count) + time.sleep(1) + except KeyboardInterrupt: + wsClient.close() + + if wsClient.error: + sys.exit(1) + else: + sys.exit(0) From f26d97fe5351414822634d10820da76bf28a8346 Mon Sep 17 00:00:00 2001 From: Drew Rice Date: Tue, 10 Oct 2017 10:17:52 -0500 Subject: [PATCH 077/174] updated WebsocketClient, requirements.txt --- gdax/websocket_client.py | 11 ++++++++--- requirements.txt | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index ab0f96ff..ca8e4f14 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -12,10 +12,11 @@ import time from threading import Thread from websocket import create_connection, WebSocketConnectionClosedException - +from pymongo import MongoClient class WebsocketClient(object): - def __init__(self, url="wss://ws-feed.gdax.com", products=None, message_type="subscribe", auth=False, api_key="", api_secret="", api_passphrase=""): + def __init__(self, url="wss://ws-feed.gdax.com", products=None, message_type="subscribe", + mongo_collection=None, should_print=True, auth=False, api_key="", api_secret="", api_passphrase=""): self.url = url self.products = products self.type = message_type @@ -26,6 +27,7 @@ def __init__(self, url="wss://ws-feed.gdax.com", products=None, message_type="su self.api_key = api_key self.api_secret = api_secret self.api_passphrase = api_passphrase + self.mongo_collection = mongo_collection def start(self): def _go(): @@ -99,7 +101,10 @@ def on_close(self): print("\n-- Socket Closed --") def on_message(self, msg): - print(msg) + if should_print: + print(msg) + if self.mongo_collection: # dump JSON to given mongo collection + self.mongo_collection.insert_one(msg) def on_error(self, e): print(e) diff --git a/requirements.txt b/requirements.txt index 40bc1f53..0626961c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ bintrees==2.0.7 requests==2.13.0 six==1.10.0 websocket-client==0.40.0 +pymongo \ No newline at end of file From ceae1239facff2d91e385025719e13bc3ce9504b Mon Sep 17 00:00:00 2001 From: Drew Rice Date: Tue, 10 Oct 2017 10:20:09 -0500 Subject: [PATCH 078/174] clarified author comments --- gdax/websocket_client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index ca8e4f14..92531d9e 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -1,6 +1,7 @@ -# # gdax/WebsocketClient.py -# Daniel Paquin +# original author: Daniel Paquin +# mongo "support" added by Drew Rice +# # # Template object to receive messages from the gdax Websocket Feed From 46deab4e6a52f19588e41f20df2f4f6e9df16538 Mon Sep 17 00:00:00 2001 From: Drew Rice Date: Tue, 10 Oct 2017 11:08:11 -0500 Subject: [PATCH 079/174] slight refactoring, README instructions for PR#125 --- README.md | 92 ++++++++++++++++++++++++---------------- gdax/websocket_client.py | 9 ++-- 2 files changed, 61 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 75adb699..60fde418 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,24 @@ # gdax-python -The Python client for the [GDAX API](https://docs.gdax.com/) (formerly known as +The Python client for the [GDAX API](https://docs.gdax.com/) (formerly known as the Coinbase Exchange API) ##### Provided under MIT License by Daniel Paquin. -*Note: this library may be subtly broken or buggy. The code is released under +*Note: this library may be subtly broken or buggy. The code is released under the MIT License – please take the following message to heart:* -> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## Benefits - A simple to use python wrapper for both public and authenticated endpoints. -- In about 10 minutes, you could be programmatically trading on one of the +- In about 10 minutes, you could be programmatically trading on one of the largest Bitcoin exchanges in the *world*! -- Do not worry about handling the nuances of the API with easy-to-use methods +- Do not worry about handling the nuances of the API with easy-to-use methods for every API endpoint. -- Gain an advantage in the market by getting under the hood of GDAX to learn +- Gain an advantage in the market by getting under the hood of GDAX to learn what and who is *really* behind every tick. ## Under Development @@ -27,10 +27,10 @@ what and who is *really* behind every tick. - FIX API Client **Looking for assistance** ## Getting Started -This README is documentation on the syntax of the python client presented in +This README is documentation on the syntax of the python client presented in this repository. See function docstrings for full syntax details. -**This API attempts to present a clean interface to GDAX, but in order to use it -to its full potential, you must familiarize yourself with the official GDAX +**This API attempts to present a clean interface to GDAX, but in order to use it +to its full potential, you must familiarize yourself with the official GDAX documentation.** - https://docs.gdax.com/ @@ -41,7 +41,7 @@ pip install gdax ``` ### Public Client -Only some endpoints in the API are available to everyone. The public endpoints +Only some endpoints in the API are available to everyone. The public endpoints can be reached using ```PublicClient``` ```python @@ -111,28 +111,28 @@ integrate both into your script. import gdax auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase) # Set a default product -auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase, +auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase, product_id="ETH-USD") # Use the sandbox API (requires a different set of API access credentials) -auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase, +auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase, api_url="https://api-public.sandbox.gdax.com") ``` ### Pagination -Some calls are [paginated](https://docs.gdax.com/#pagination), meaning multiple -calls must be made to receive the full set of data. Each page/request is a list -of dict objects that are then appended to a master list, making it easy to -navigate pages (e.g. ```request[0]``` would return the first page of data in the -example below). *This feature is under consideration for redesign. Please +Some calls are [paginated](https://docs.gdax.com/#pagination), meaning multiple +calls must be made to receive the full set of data. Each page/request is a list +of dict objects that are then appended to a master list, making it easy to +navigate pages (e.g. ```request[0]``` would return the first page of data in the +example below). *This feature is under consideration for redesign. Please provide feedback if you have issues or suggestions* ```python request = auth_client.get_fills(limit=100) request[0] # Page 1 always present request[1] # Page 2+ present only if the data exists ``` -It should be noted that limit does not behave exactly as the official -documentation specifies. If you request a limit and that limit is met, -additional pages will not be returned. This is to ensure speedy response times +It should be noted that limit does not behave exactly as the official +documentation specifies. If you request a limit and that limit is met, +additional pages will not be returned. This is to ensure speedy response times when less data is preferred. ### AuthenticatedClient Methods @@ -217,7 +217,7 @@ auth_client.withdraw(withdrawParams) ``` ### WebsocketClient -If you would like to receive real-time market updates, you must subscribe to the +If you would like to receive real-time market updates, you must subscribe to the [websocket feed](https://docs.gdax.com/#websocket-feed). #### Subscribe to a single product @@ -233,21 +233,38 @@ wsClient.close() ```python import gdax # Paramaters are optional -wsClient = gdax.WebsocketClient(url="wss://ws-feed.gdax.com", +wsClient = gdax.WebsocketClient(url="wss://ws-feed.gdax.com", products=["BTC-USD", "ETH-USD"]) # Do other stuff... wsClient.close() ``` +### WebsocketClient + Mongodb +The ```WebsocketClient``` now supports data gathering via MongoDB. Given a +MongoDB collection, the ```WebsocketClient``` will stream results directly into +the database collection. +```python +# import PyMongo and connect to a local, running Mongo instance +from pymongo import MongoClient +mongo_client = MongoClient('mongodb://localhost:27017/') +# specify the database and collection +db = mongo_client.cryptocurrency_database +BTC_collection = db.BTC_collection +# instantiate a WebsocketClient instance, with a Mongo collection as a parameter +wsClient = WebsocketClient(url="wss://ws-feed.gdax.com", products="BTC-USD", + mongo_collection=BTC_collection, should_print=False) +wsClient.start() +``` + ### WebsocketClient Methods -The ```WebsocketClient``` subscribes in a separate thread upon initialization. -There are three methods which you could overwrite (before initialization) so it -can react to the data streaming in. The current client is a template used for +The ```WebsocketClient``` subscribes in a separate thread upon initialization. +There are three methods which you could overwrite (before initialization) so it +can react to the data streaming in. The current client is a template used for illustration purposes only. -- onOpen - called once, *immediately before* the socket connection is made, this +- onOpen - called once, *immediately before* the socket connection is made, this is where you want to add inital parameters. -- onMessage - called once for every message that arrives and accepts one +- onMessage - called once for every message that arrives and accepts one argument that contains the message of dict type. - onClose - called once after the websocket has been closed. - close - call this method to close the websocket connection (do not overwrite). @@ -262,7 +279,7 @@ class myWebsocketClient(gdax.WebsocketClient): def on_message(self, msg): self.message_count += 1 if 'price' in msg and 'type' in msg: - print ("Message type:", msg["type"], + print ("Message type:", msg["type"], "\t@ {:.3f}".format(float(msg["price"]))) def on_close(self): print("-- Goodbye! --") @@ -275,16 +292,17 @@ while (wsClient.message_count < 500): time.sleep(1) wsClient.close() ``` + ## Testing -A test suite is under development. To run the tests, start in the project +A test suite is under development. To run the tests, start in the project directory and run ``` python -m pytest ``` ### Real-time OrderBook -The ```OrderBook``` subscribes to a websocket and keeps a real-time record of -the orderbook for the product_id input. Please provide your feedback for future +The ```OrderBook``` subscribes to a websocket and keeps a real-time record of +the orderbook for the product_id input. Please provide your feedback for future improvements. ```python @@ -297,7 +315,7 @@ order_book.close() ## Change Log *1.0* **Current PyPI release** -- The first release that is not backwards compatible +- The first release that is not backwards compatible - Refactored to follow PEP 8 Standards - Improved Documentation @@ -312,7 +330,7 @@ order_book.close() - Added additional API functionality such as cancelAll() and ETH withdrawal. *0.2.1* -- Allowed ```WebsocketClient``` to operate intuitively and restructured example +- Allowed ```WebsocketClient``` to operate intuitively and restructured example workflow. *0.2.0* diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index 92531d9e..609f42cc 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -28,6 +28,7 @@ def __init__(self, url="wss://ws-feed.gdax.com", products=None, message_type="su self.api_key = api_key self.api_secret = api_secret self.api_passphrase = api_passphrase + self.should_print = should_print self.mongo_collection = mongo_collection def start(self): @@ -96,13 +97,15 @@ def close(self): pass def on_open(self): - print("-- Subscribed! --\n") + if self.should_print: + print("-- Subscribed! --\n") def on_close(self): - print("\n-- Socket Closed --") + if self.should_print: + print("\n-- Socket Closed --") def on_message(self, msg): - if should_print: + if self.should_print: print(msg) if self.mongo_collection: # dump JSON to given mongo collection self.mongo_collection.insert_one(msg) From 28e6299d8238ba70ae2a4760c67ad2a00b6fbc45 Mon Sep 17 00:00:00 2001 From: Lawrence Zhou Date: Fri, 13 Oct 2017 07:14:38 +0000 Subject: [PATCH 080/174] minor changes to allow channels (currently, websockets only works with full) --- gdax/websocket_client.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index 609f42cc..03497a5b 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -17,9 +17,10 @@ class WebsocketClient(object): def __init__(self, url="wss://ws-feed.gdax.com", products=None, message_type="subscribe", - mongo_collection=None, should_print=True, auth=False, api_key="", api_secret="", api_passphrase=""): + mongo_collection=None, should_print=True, auth=False, api_key="", api_secret="", api_passphrase="", channels=None): self.url = url self.products = products + self.channels = channels self.type = message_type self.stop = False self.ws = None @@ -50,7 +51,12 @@ def _connect(self): if self.url[-1] == "/": self.url = self.url[:-1] - sub_params = {'type': 'subscribe', 'product_ids': self.products} + if self.channels is None: + sub_params = {'type': 'subscribe', 'product_ids': self.products} + else: + sub_params = {'type': 'subscribe', 'product_ids': self.products, 'channels': self.channels} + + if self.auth: timestamp = str(time.time()) message = timestamp + 'GET' + '/users/self' From 193766ab10b5e2199381e229966210b71d100057 Mon Sep 17 00:00:00 2001 From: Drew Rice Date: Wed, 18 Oct 2017 12:43:05 -0500 Subject: [PATCH 081/174] adds "pymongo==3.5.1" to `setup.py`, README spacing --- README.md | 8 +++++--- contributors.txt | 1 + gdax/websocket_client.py | 5 +++-- requirements.txt | 2 +- setup.py | 1 + 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 31be83b1..a6df7718 100644 --- a/README.md +++ b/README.md @@ -240,14 +240,16 @@ wsClient.close() ### WebsocketClient + Mongodb The ```WebsocketClient``` now supports data gathering via MongoDB. Given a MongoDB collection, the ```WebsocketClient``` will stream results directly into -the database collection. +the database collection. ```python # import PyMongo and connect to a local, running Mongo instance from pymongo import MongoClient mongo_client = MongoClient('mongodb://localhost:27017/') + # specify the database and collection db = mongo_client.cryptocurrency_database BTC_collection = db.BTC_collection + # instantiate a WebsocketClient instance, with a Mongo collection as a parameter wsClient = WebsocketClient(url="wss://ws-feed.gdax.com", products="BTC-USD", mongo_collection=BTC_collection, should_print=False) @@ -261,9 +263,9 @@ can react to the data streaming in. The current client is a template used for illustration purposes only. -- onOpen - called once, *immediately before* the socket connection is made, this +- onOpen - called once, *immediately before* the socket connection is made, this is where you want to add initial parameters. -- onMessage - called once for every message that arrives and accepts one +- onMessage - called once for every message that arrives and accepts one argument that contains the message of dict type. - onClose - called once after the websocket has been closed. - close - call this method to close the websocket connection (do not overwrite). diff --git a/contributors.txt b/contributors.txt index a2b3a3fe..b1e5c495 100644 --- a/contributors.txt +++ b/contributors.txt @@ -3,3 +3,4 @@ Leonard Lin Jeff Gibson David Caseria Paul Mestemaker +Drew Rice \ No newline at end of file diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index 609f42cc..307fd07d 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -16,8 +16,9 @@ from pymongo import MongoClient class WebsocketClient(object): - def __init__(self, url="wss://ws-feed.gdax.com", products=None, message_type="subscribe", - mongo_collection=None, should_print=True, auth=False, api_key="", api_secret="", api_passphrase=""): + def __init__(self, url="wss://ws-feed.gdax.com", products=None, + message_type="subscribe", mongo_collection=None, should_print=True, + auth=False, api_key="", api_secret="", api_passphrase=""): self.url = url self.products = products self.type = message_type diff --git a/requirements.txt b/requirements.txt index 0626961c..2ef54de0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ bintrees==2.0.7 requests==2.13.0 six==1.10.0 websocket-client==0.40.0 -pymongo \ No newline at end of file +pymongo==3.5.1 \ No newline at end of file diff --git a/setup.py b/setup.py index dbeee918..368ee131 100644 --- a/setup.py +++ b/setup.py @@ -7,6 +7,7 @@ 'requests==2.13.0', 'six==1.10.0', 'websocket-client==0.40.0', + 'pymongo==3.5.1' ] tests_require = [ From 2e7bda851b538b8e17f5c2dae9d79327d9b1327f Mon Sep 17 00:00:00 2001 From: Drew Rice Date: Wed, 18 Oct 2017 12:52:19 -0500 Subject: [PATCH 082/174] clarifies the Mongo instructions in README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a6df7718..64ea76e6 100644 --- a/README.md +++ b/README.md @@ -244,6 +244,7 @@ the database collection. ```python # import PyMongo and connect to a local, running Mongo instance from pymongo import MongoClient +import gdax mongo_client = MongoClient('mongodb://localhost:27017/') # specify the database and collection @@ -251,7 +252,7 @@ db = mongo_client.cryptocurrency_database BTC_collection = db.BTC_collection # instantiate a WebsocketClient instance, with a Mongo collection as a parameter -wsClient = WebsocketClient(url="wss://ws-feed.gdax.com", products="BTC-USD", +wsClient = gdax.WebsocketClient(url="wss://ws-feed.gdax.com", products="BTC-USD", mongo_collection=BTC_collection, should_print=False) wsClient.start() ``` From 2e1189edaefc4fbd86061c582f2a0140529bb684 Mon Sep 17 00:00:00 2001 From: kzielinski Date: Tue, 31 Oct 2017 21:41:44 -0500 Subject: [PATCH 083/174] ISSUE-128: fixed auth keys and encoding issues with websocket client --- gdax/websocket_client.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index 69e23b66..4f05ad0f 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -15,10 +15,10 @@ from websocket import create_connection, WebSocketConnectionClosedException from pymongo import MongoClient -class WebsocketClient(object): - def __init__(self, url="wss://ws-feed.gdax.com", products=None, message_type="subscribe", mongo_collection=None, - should_print=True, auth=False, api_key="", api_secret="", api_passphrase="", channels=None): +class WebsocketClient(object): + def __init__(self, url="wss://ws-feed.gdax.com", products=None, message_type="subscribe", mongo_collection=None, + should_print=True, auth=False, api_key="", api_secret="", api_passphrase="", channels=None): self.url = url self.products = products self.channels = channels @@ -55,10 +55,9 @@ def _connect(self): self.url = self.url[:-1] if self.channels is None: - sub_params = {'type': 'subscribe', 'product_ids': self.products} + sub_params = {'type': 'subscribe', 'product_ids': self.products} else: - sub_params = {'type': 'subscribe', 'product_ids': self.products, 'channels': self.channels} - + sub_params = {'type': 'subscribe', 'product_ids': self.products, 'channels': self.channels} if self.auth: timestamp = str(time.time()) @@ -66,11 +65,11 @@ def _connect(self): message = message.encode('ascii') hmac_key = base64.b64decode(self.api_secret) signature = hmac.new(hmac_key, message, hashlib.sha256) - signature_b64 = base64.b64encode(signature.digest()) - sub_params['signature'] = signature_b64 - sub_params['key'] = self.api_key - sub_params['passphrase'] = self.api_passphrase - sub_params['timestamp'] = timestamp + signature_b64 = base64.b64encode(signature.digest()).decode("utf-8") + sub_params['CB-ACCESS-SIGN'] = signature_b64 + sub_params['CB-ACCESS-KEY'] = self.api_key + sub_params['CB-ACCESS-PASSPHRASE'] = self.api_passphrase + sub_params['CB-ACCESS-TIMESTAMP'] = timestamp self.ws = create_connection(self.url) self.ws.send(json.dumps(sub_params)) @@ -81,7 +80,6 @@ def _connect(self): sub_params = {"type": "heartbeat", "on": False} self.ws.send(json.dumps(sub_params)) - def _listen(self): while not self.stop: try: @@ -123,7 +121,7 @@ def on_close(self): def on_message(self, msg): if self.should_print: print(msg) - if self.mongo_collection: # dump JSON to given mongo collection + if self.mongo_collection: # dump JSON to given mongo collection self.mongo_collection.insert_one(msg) def on_error(self, e, data=None): @@ -131,11 +129,13 @@ def on_error(self, e, data=None): self.stop print('{} - data: {}'.format(e, data)) + if __name__ == "__main__": import sys import gdax import time + class MyWebsocketClient(gdax.WebsocketClient): def on_open(self): self.url = "wss://ws-feed.gdax.com/" @@ -150,6 +150,7 @@ def on_message(self, msg): def on_close(self): print("-- Goodbye! --") + wsClient = MyWebsocketClient() wsClient.start() print(wsClient.url, wsClient.products) From d4c4521750c7613b6f2cbaf54ed05b5fae84c973 Mon Sep 17 00:00:00 2001 From: kzielinski Date: Wed, 1 Nov 2017 00:55:22 -0500 Subject: [PATCH 084/174] ISSUE-128: refactored to prevent duplicate code --- gdax/authenticated_client.py | 27 ++------------------------- gdax/gdax_auth.py | 34 ++++++++++++++++++++++++++++++++++ gdax/websocket_client.py | 10 ++-------- 3 files changed, 38 insertions(+), 33 deletions(-) create mode 100644 gdax/gdax_auth.py diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index 6e00e616..2898a772 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -12,6 +12,7 @@ import json from requests.auth import AuthBase from gdax.public_client import PublicClient +from gdax.gdax_auth import GdaxAuth class AuthenticatedClient(PublicClient): @@ -288,28 +289,4 @@ def get_report(self, report_id=""): def get_trailing_volume(self): r = requests.get(self.url + "/users/self/trailing-volume", auth=self.auth, timeout=30) # r.raise_for_status() - return r.json() - - -class GdaxAuth(AuthBase): - # Provided by gdax: https://docs.gdax.com/#signing-a-message - def __init__(self, api_key, secret_key, passphrase): - self.api_key = api_key - self.secret_key = secret_key - self.passphrase = passphrase - - def __call__(self, request): - timestamp = str(time.time()) - message = timestamp + request.method + request.path_url + (request.body or '') - message = message.encode('ascii') - hmac_key = base64.b64decode(self.secret_key) - signature = hmac.new(hmac_key, message, hashlib.sha256) - signature_b64 = base64.b64encode(signature.digest()) - request.headers.update({ - 'Content-Type': 'Application/JSON', - 'CB-ACCESS-SIGN': signature_b64, - 'CB-ACCESS-TIMESTAMP': timestamp, - 'CB-ACCESS-KEY': self.api_key, - 'CB-ACCESS-PASSPHRASE': self.passphrase - }) - return request + return r.json() \ No newline at end of file diff --git a/gdax/gdax_auth.py b/gdax/gdax_auth.py new file mode 100644 index 00000000..14757c3c --- /dev/null +++ b/gdax/gdax_auth.py @@ -0,0 +1,34 @@ +import hmac +import hashlib +import time +import base64 +from requests.auth import AuthBase + + +class GdaxAuth(AuthBase): + # Provided by gdax: https://docs.gdax.com/#signing-a-message + def __init__(self, api_key, secret_key, passphrase): + self.api_key = api_key + self.secret_key = secret_key + self.passphrase = passphrase + + def __call__(self, request): + timestamp = str(time.time()) + message = timestamp + request.method + request.path_url + (request.body or '') + request.headers.update(get_auth_headers(timestamp, message, self.api_key, self.secret_key, + self.passphrase)) + return request + + +def get_auth_headers(timestamp, message, api_key, secret_key, passphrase): + message = message.encode('ascii') + hmac_key = base64.b64decode(secret_key) + signature = hmac.new(hmac_key, message, hashlib.sha256) + signature_b64 = base64.b64encode(signature.digest()).decode('utf-8') + return { + 'Content-Type': 'Application/JSON', + 'CB-ACCESS-SIGN': signature_b64, + 'CB-ACCESS-TIMESTAMP': timestamp, + 'CB-ACCESS-KEY': api_key, + 'CB-ACCESS-PASSPHRASE': passphrase + } diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index 4f05ad0f..0c679e10 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -14,6 +14,7 @@ from threading import Thread from websocket import create_connection, WebSocketConnectionClosedException from pymongo import MongoClient +from gdax.gdax_auth import get_auth_headers class WebsocketClient(object): @@ -62,14 +63,7 @@ def _connect(self): if self.auth: timestamp = str(time.time()) message = timestamp + 'GET' + '/users/self' - message = message.encode('ascii') - hmac_key = base64.b64decode(self.api_secret) - signature = hmac.new(hmac_key, message, hashlib.sha256) - signature_b64 = base64.b64encode(signature.digest()).decode("utf-8") - sub_params['CB-ACCESS-SIGN'] = signature_b64 - sub_params['CB-ACCESS-KEY'] = self.api_key - sub_params['CB-ACCESS-PASSPHRASE'] = self.api_passphrase - sub_params['CB-ACCESS-TIMESTAMP'] = timestamp + sub_params.update(get_auth_headers(timestamp, message, self.api_key, self.api_secret, self.api_passphrase)) self.ws = create_connection(self.url) self.ws.send(json.dumps(sub_params)) From e0612e2eaa1f02ff7de2c44ecb9fa3396ec7f8d8 Mon Sep 17 00:00:00 2001 From: Hegusung Date: Tue, 21 Nov 2017 10:00:47 +0100 Subject: [PATCH 085/174] Support status param in get_orders --- gdax/authenticated_client.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index 2898a772..5128a0ad 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -100,23 +100,32 @@ def get_order(self, order_id): # r.raise_for_status() return r.json() - def get_orders(self, product_id=''): + def get_orders(self, product_id='', status=[]): result = [] url = self.url + '/orders/' + params = {} if product_id: - url += "?product_id={}&".format(product_id) - r = requests.get(url, auth=self.auth, timeout=30) + params["product_id"] = product_id + if status: + params["status"] = status + r = requests.get(url, auth=self.auth, params=params, timeout=30) # r.raise_for_status() result.append(r.json()) if 'cb-after' in r.headers: - self.paginate_orders(product_id, result, r.headers['cb-after']) + self.paginate_orders(product_id, status, result, r.headers['cb-after']) return result - def paginate_orders(self, product_id, result, after): - url = self.url + '/orders?after={}&'.format(str(after)) + def paginate_orders(self, product_id, status, result, after): + url = self.url + '/orders' + + params = { + "after": str(after), + } if product_id: - url += "product_id={}&".format(product_id) - r = requests.get(url, auth=self.auth, timeout=30) + params["product_id"] = product_id + if status: + params["status"] = status + r = requests.get(url, auth=self.auth, params=params, timeout=30) # r.raise_for_status() if r.json(): result.append(r.json()) @@ -289,4 +298,4 @@ def get_report(self, report_id=""): def get_trailing_volume(self): r = requests.get(self.url + "/users/self/trailing-volume", auth=self.auth, timeout=30) # r.raise_for_status() - return r.json() \ No newline at end of file + return r.json() From c9cd0d1b768a1dc26841776eea1662909bd7a6bb Mon Sep 17 00:00:00 2001 From: redmoonshine <33815561+redmoonshine@users.noreply.github.com> Date: Fri, 24 Nov 2017 15:56:35 -0800 Subject: [PATCH 086/174] Stop should be true when on error --- gdax/websocket_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index 0c679e10..de468e7e 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -120,7 +120,7 @@ def on_message(self, msg): def on_error(self, e, data=None): self.error = e - self.stop + self.stop = True print('{} - data: {}'.format(e, data)) From 2e5c7104679a7e2616ec044baec669a2027ea313 Mon Sep 17 00:00:00 2001 From: John Lane Date: Wed, 29 Nov 2017 02:24:26 -0600 Subject: [PATCH 087/174] Improvements to gdax public client Consolidate all requests.get into single call Removing duplicate code on all the Public client gets. Get product order book should check for invalid levels Default to level 1 Add pytest-cov pytest-cov provides coverage reports Parametrize order book test Test get product order book with multiple levels Add wait between tests to avoid rate limits Parameterize historic rates test Add parameters for start, end and granularity --- gdax/public_client.py | 47 +++++++++++++++---------------------- pytest.ini | 3 +++ requirements.txt | 4 +++- tests/test_public_client.py | 29 ++++++++++++++++++----- 4 files changed, 48 insertions(+), 35 deletions(-) create mode 100644 pytest.ini diff --git a/gdax/public_client.py b/gdax/public_client.py index 91d4f5cf..305f67b0 100644 --- a/gdax/public_client.py +++ b/gdax/public_client.py @@ -27,6 +27,13 @@ def __init__(self, api_url='https://api.gdax.com'): """ self.url = api_url.rstrip('/') + def _get(self, path, params=None): + """Perform get request""" + + r = requests.get(self.url + path, params=params, timeout=30) + # r.raise_for_status() + return r.json() + def get_products(self): """Get a list of available currency pairs for trading. @@ -45,9 +52,7 @@ def get_products(self): ] """ - r = requests.get(self.url + '/products', timeout=30) - # r.raise_for_status() - return r.json() + return self._get('/products') def get_product_order_book(self, product_id, level=1): """Get a list of open orders for a product. @@ -84,11 +89,10 @@ def get_product_order_book(self, product_id, level=1): } """ - params = {'level': level} - r = requests.get(self.url + '/products/{}/book' - .format(product_id), params=params, timeout=30) - # r.raise_for_status() - return r.json() + + # Supported levels are 1, 2 or 3 + level = level if level in range(1, 4) else 1 + return self._get('/products/{}/book'.format(str(product_id)), params={'level': level}) def get_product_ticker(self, product_id): """Snapshot about the last trade (tick), best bid/ask and 24h volume. @@ -112,10 +116,7 @@ def get_product_ticker(self, product_id): } """ - r = requests.get(self.url + '/products/{}/ticker' - .format(product_id), timeout=30) - # r.raise_for_status() - return r.json() + return self._get('/products/{}/ticker'.format(str(product_id))) def get_product_trades(self, product_id): """List the latest trades for a product. @@ -140,9 +141,7 @@ def get_product_trades(self, product_id): }] """ - r = requests.get(self.url + '/products/{}/trades'.format(product_id), timeout=30) - # r.raise_for_status() - return r.json() + return self._get('/products/{}/trades'.format(str(product_id))) def get_product_historic_rates(self, product_id, start=None, end=None, granularity=None): @@ -188,10 +187,8 @@ def get_product_historic_rates(self, product_id, start=None, end=None, params['end'] = end if granularity is not None: params['granularity'] = granularity - r = requests.get(self.url + '/products/{}/candles' - .format(product_id), params=params, timeout=30) - # r.raise_for_status() - return r.json() + + return self._get('/products/{}/candles'.format(str(product_id)), params=params) def get_product_24hr_stats(self, product_id): """Get 24 hr stats for the product. @@ -210,9 +207,7 @@ def get_product_24hr_stats(self, product_id): } """ - r = requests.get(self.url + '/products/{}/stats'.format(product_id), timeout=30) - # r.raise_for_status() - return r.json() + return self._get('/products/{}/stats'.format(str(product_id))) def get_currencies(self): """List known currencies. @@ -230,9 +225,7 @@ def get_currencies(self): }] """ - r = requests.get(self.url + '/currencies', timeout=30) - # r.raise_for_status() - return r.json() + return self._get('/currencies') def get_time(self): """Get the API server time. @@ -246,6 +239,4 @@ def get_time(self): } """ - r = requests.get(self.url + '/time', timeout=30) - # r.raise_for_status() - return r.json() + return self._get('/time') diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..ab35919d --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +addopts = --cov gdax/ --cov-report=term-missing +testpaths = tests \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2ef54de0..f7f58718 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,6 @@ bintrees==2.0.7 requests==2.13.0 six==1.10.0 websocket-client==0.40.0 -pymongo==3.5.1 \ No newline at end of file +pymongo==3.5.1 +pytest>=3.3.0 +pytest-cov>=2.5.0 \ No newline at end of file diff --git a/tests/test_public_client.py b/tests/test_public_client.py index 5da77c27..d33a1844 100644 --- a/tests/test_public_client.py +++ b/tests/test_public_client.py @@ -1,5 +1,6 @@ import pytest import gdax +import time @pytest.fixture(scope='module') @@ -9,18 +10,31 @@ def client(): @pytest.mark.usefixtures('client') class TestPublicClient(object): + + @staticmethod + def teardown_method(): + time.sleep(.25) # Avoid rate limit + def test_get_products(self, client): r = client.get_products() assert type(r) is list - def test_get_product_order_book(self, client): - r = client.get_product_order_book('BTC-USD') - assert type(r) is dict - r = client.get_product_order_book('BTC-USD', level=2) + @pytest.mark.parametrize('level', [1, 2, 3, None]) + def test_get_product_order_book(self, client, level): + r = client.get_product_order_book('BTC-USD', level=level) assert type(r) is dict assert 'asks' in r assert 'bids' in r + if level in (1, None) and (len(r['asks']) > 1 or len(r['bids']) > 1): + pytest.fail('Fail: Level 1 should only return the best ask and bid') + + if level is 2 and (len(r['asks']) > 50 or len(r['bids']) > 50): + pytest.fail('Fail: Level 2 should only return the top 50 asks and bids') + + if level is 2 and (len(r['asks']) < 50 or len(r['bids']) < 50): + pytest.fail('Fail: Level 3 should return the full order book') + def test_get_product_ticker(self, client): r = client.get_product_ticker('BTC-USD') assert type(r) is dict @@ -32,8 +46,11 @@ def test_get_product_trades(self, client): assert type(r) is list assert 'trade_id' in r[0] - def test_get_historic_rates(self, client): - r = client.get_product_historic_rates('BTC-USD') + @pytest.mark.parametrize('start', ('2017-11-01', None)) + @pytest.mark.parametrize('end', ('2017-11-30', None)) + @pytest.mark.parametrize('granularity', (3600, None)) + def test_get_historic_rates(self, client, start, end, granularity): + r = client.get_product_historic_rates('BTC-USD', start=start, end=end, granularity=granularity) assert type(r) is list def test_get_product_24hr_stats(self, client): From 3f1444dcf9c5881e62de4ddcd0e59468e6736452 Mon Sep 17 00:00:00 2001 From: Damian Moore Date: Wed, 6 Dec 2017 19:52:41 +0000 Subject: [PATCH 088/174] Allow setting a timeout value when intializing the clients to override the default of 30 seconds --- gdax/authenticated_client.py | 61 ++++++++++++++++++------------------ gdax/public_client.py | 5 +-- 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index 5128a0ad..c9e3dd52 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -16,12 +16,13 @@ class AuthenticatedClient(PublicClient): - def __init__(self, key, b64secret, passphrase, api_url="https://api.gdax.com"): + def __init__(self, key, b64secret, passphrase, api_url="https://api.gdax.com", timeout=30): super(AuthenticatedClient, self).__init__(api_url) self.auth = GdaxAuth(key, b64secret, passphrase) + self.timeout = timeout def get_account(self, account_id): - r = requests.get(self.url + '/accounts/' + account_id, auth=self.auth, timeout=30) + r = requests.get(self.url + '/accounts/' + account_id, auth=self.auth, timeout=self.timeout) # r.raise_for_status() return r.json() @@ -30,7 +31,7 @@ def get_accounts(self): def get_account_history(self, account_id): result = [] - r = requests.get(self.url + '/accounts/{}/ledger'.format(account_id), auth=self.auth, timeout=30) + r = requests.get(self.url + '/accounts/{}/ledger'.format(account_id), auth=self.auth, timeout=self.timeout) # r.raise_for_status() result.append(r.json()) if "cb-after" in r.headers: @@ -38,7 +39,7 @@ def get_account_history(self, account_id): return result def history_pagination(self, account_id, result, after): - r = requests.get(self.url + '/accounts/{}/ledger?after={}'.format(account_id, str(after)), auth=self.auth, timeout=30) + r = requests.get(self.url + '/accounts/{}/ledger?after={}'.format(account_id, str(after)), auth=self.auth, timeout=self.timeout) # r.raise_for_status() if r.json(): result.append(r.json()) @@ -48,7 +49,7 @@ def history_pagination(self, account_id, result, after): def get_account_holds(self, account_id): result = [] - r = requests.get(self.url + '/accounts/{}/holds'.format(account_id), auth=self.auth, timeout=30) + r = requests.get(self.url + '/accounts/{}/holds'.format(account_id), auth=self.auth, timeout=self.timeout) # r.raise_for_status() result.append(r.json()) if "cb-after" in r.headers: @@ -56,7 +57,7 @@ def get_account_holds(self, account_id): return result def holds_pagination(self, account_id, result, after): - r = requests.get(self.url + '/accounts/{}/holds?after={}'.format(account_id, str(after)), auth=self.auth, timeout=30) + r = requests.get(self.url + '/accounts/{}/holds?after={}'.format(account_id, str(after)), auth=self.auth, timeout=self.timeout) # r.raise_for_status() if r.json(): result.append(r.json()) @@ -71,7 +72,7 @@ def buy(self, **kwargs): r = requests.post(self.url + '/orders', data=json.dumps(kwargs), auth=self.auth, - timeout=30) + timeout=self.timeout) return r.json() def sell(self, **kwargs): @@ -79,11 +80,11 @@ def sell(self, **kwargs): r = requests.post(self.url + '/orders', data=json.dumps(kwargs), auth=self.auth, - timeout=30) + timeout=self.timeout) return r.json() def cancel_order(self, order_id): - r = requests.delete(self.url + '/orders/' + order_id, auth=self.auth, timeout=30) + r = requests.delete(self.url + '/orders/' + order_id, auth=self.auth, timeout=self.timeout) # r.raise_for_status() return r.json() @@ -91,12 +92,12 @@ def cancel_all(self, product_id=''): url = self.url + '/orders/' if product_id: url += "?product_id={}&".format(str(product_id)) - r = requests.delete(url, auth=self.auth, timeout=30) + r = requests.delete(url, auth=self.auth, timeout=self.timeout) # r.raise_for_status() return r.json() def get_order(self, order_id): - r = requests.get(self.url + '/orders/' + order_id, auth=self.auth, timeout=30) + r = requests.get(self.url + '/orders/' + order_id, auth=self.auth, timeout=self.timeout) # r.raise_for_status() return r.json() @@ -108,7 +109,7 @@ def get_orders(self, product_id='', status=[]): params["product_id"] = product_id if status: params["status"] = status - r = requests.get(url, auth=self.auth, params=params, timeout=30) + r = requests.get(url, auth=self.auth, params=params, timeout=self.timeout) # r.raise_for_status() result.append(r.json()) if 'cb-after' in r.headers: @@ -125,7 +126,7 @@ def paginate_orders(self, product_id, status, result, after): params["product_id"] = product_id if status: params["status"] = status - r = requests.get(url, auth=self.auth, params=params, timeout=30) + r = requests.get(url, auth=self.auth, params=params, timeout=self.timeout) # r.raise_for_status() if r.json(): result.append(r.json()) @@ -146,7 +147,7 @@ def get_fills(self, order_id='', product_id='', before='', after='', limit=''): url += "after={}&".format(str(after)) if limit: url += "limit={}&".format(str(limit)) - r = requests.get(url, auth=self.auth, timeout=30) + r = requests.get(url, auth=self.auth, timeout=self.timeout) # r.raise_for_status() result.append(r.json()) if 'cb-after' in r.headers and limit is not len(r.json()): @@ -159,7 +160,7 @@ def paginate_fills(self, result, after, order_id='', product_id=''): url += "order_id={}&".format(str(order_id)) if product_id: url += "product_id={}&".format(product_id) - r = requests.get(url, auth=self.auth, timeout=30) + r = requests.get(url, auth=self.auth, timeout=self.timeout) # r.raise_for_status() if r.json(): result.append(r.json()) @@ -175,7 +176,7 @@ def get_fundings(self, result='', status='', after=''): url += "status={}&".format(str(status)) if after: url += 'after={}&'.format(str(after)) - r = requests.get(url, auth=self.auth, timeout=30) + r = requests.get(url, auth=self.auth, timeout=self.timeout) # r.raise_for_status() result.append(r.json()) if 'cb-after' in r.headers: @@ -187,7 +188,7 @@ def repay_funding(self, amount='', currency=''): "amount": amount, "currency": currency # example: USD } - r = requests.post(self.url + "/funding/repay", data=json.dumps(payload), auth=self.auth, timeout=30) + r = requests.post(self.url + "/funding/repay", data=json.dumps(payload), auth=self.auth, timeout=self.timeout) # r.raise_for_status() return r.json() @@ -198,12 +199,12 @@ def margin_transfer(self, margin_profile_id="", transfer_type="", currency="", a "currency": currency, # example: USD "amount": amount } - r = requests.post(self.url + "/profiles/margin-transfer", data=json.dumps(payload), auth=self.auth, timeout=30) + r = requests.post(self.url + "/profiles/margin-transfer", data=json.dumps(payload), auth=self.auth, timeout=self.timeout) # r.raise_for_status() return r.json() def get_position(self): - r = requests.get(self.url + "/position", auth=self.auth, timeout=30) + r = requests.get(self.url + "/position", auth=self.auth, timeout=self.timeout) # r.raise_for_status() return r.json() @@ -211,7 +212,7 @@ def close_position(self, repay_only=""): payload = { "repay_only": repay_only or False } - r = requests.post(self.url + "/position/close", data=json.dumps(payload), auth=self.auth, timeout=30) + r = requests.post(self.url + "/position/close", data=json.dumps(payload), auth=self.auth, timeout=self.timeout) # r.raise_for_status() return r.json() @@ -221,7 +222,7 @@ def deposit(self, amount="", currency="", payment_method_id=""): "currency": currency, "payment_method_id": payment_method_id } - r = requests.post(self.url + "/deposits/payment-method", data=json.dumps(payload), auth=self.auth, timeout=30) + r = requests.post(self.url + "/deposits/payment-method", data=json.dumps(payload), auth=self.auth, timeout=self.timeout) # r.raise_for_status() return r.json() @@ -231,7 +232,7 @@ def coinbase_deposit(self, amount="", currency="", coinbase_account_id=""): "currency": currency, "coinbase_account_id": coinbase_account_id } - r = requests.post(self.url + "/deposits/coinbase-account", data=json.dumps(payload), auth=self.auth, timeout=30) + r = requests.post(self.url + "/deposits/coinbase-account", data=json.dumps(payload), auth=self.auth, timeout=self.timeout) # r.raise_for_status() return r.json() @@ -241,7 +242,7 @@ def withdraw(self, amount="", currency="", payment_method_id=""): "currency": currency, "payment_method_id": payment_method_id } - r = requests.post(self.url + "/withdrawals/payment-method", data=json.dumps(payload), auth=self.auth, timeout=30) + r = requests.post(self.url + "/withdrawals/payment-method", data=json.dumps(payload), auth=self.auth, timeout=self.timeout) # r.raise_for_status() return r.json() @@ -251,7 +252,7 @@ def coinbase_withdraw(self, amount="", currency="", coinbase_account_id=""): "currency": currency, "coinbase_account_id": coinbase_account_id } - r = requests.post(self.url + "/withdrawals/coinbase", data=json.dumps(payload), auth=self.auth, timeout=30) + r = requests.post(self.url + "/withdrawals/coinbase", data=json.dumps(payload), auth=self.auth, timeout=self.timeout) # r.raise_for_status() return r.json() @@ -261,17 +262,17 @@ def crypto_withdraw(self, amount="", currency="", crypto_address=""): "currency": currency, "crypto_address": crypto_address } - r = requests.post(self.url + "/withdrawals/crypto", data=json.dumps(payload), auth=self.auth, timeout=30) + r = requests.post(self.url + "/withdrawals/crypto", data=json.dumps(payload), auth=self.auth, timeout=self.timeout) # r.raise_for_status() return r.json() def get_payment_methods(self): - r = requests.get(self.url + "/payment-methods", auth=self.auth, timeout=30) + r = requests.get(self.url + "/payment-methods", auth=self.auth, timeout=self.timeout) # r.raise_for_status() return r.json() def get_coinbase_accounts(self): - r = requests.get(self.url + "/coinbase-accounts", auth=self.auth, timeout=30) + r = requests.get(self.url + "/coinbase-accounts", auth=self.auth, timeout=self.timeout) # r.raise_for_status() return r.json() @@ -286,16 +287,16 @@ def create_report(self, report_type="", start_date="", end_date="", product_id=" "format": report_format, "email": email } - r = requests.post(self.url + "/reports", data=json.dumps(payload), auth=self.auth, timeout=30) + r = requests.post(self.url + "/reports", data=json.dumps(payload), auth=self.auth, timeout=self.timeout) # r.raise_for_status() return r.json() def get_report(self, report_id=""): - r = requests.get(self.url + "/reports/" + report_id, auth=self.auth, timeout=30) + r = requests.get(self.url + "/reports/" + report_id, auth=self.auth, timeout=self.timeout) # r.raise_for_status() return r.json() def get_trailing_volume(self): - r = requests.get(self.url + "/users/self/trailing-volume", auth=self.auth, timeout=30) + r = requests.get(self.url + "/users/self/trailing-volume", auth=self.auth, timeout=self.timeout) # r.raise_for_status() return r.json() diff --git a/gdax/public_client.py b/gdax/public_client.py index 305f67b0..79fef798 100644 --- a/gdax/public_client.py +++ b/gdax/public_client.py @@ -18,7 +18,7 @@ class PublicClient(object): """ - def __init__(self, api_url='https://api.gdax.com'): + def __init__(self, api_url='https://api.gdax.com', timeout=30): """Create GDAX API public client. Args: @@ -26,11 +26,12 @@ def __init__(self, api_url='https://api.gdax.com'): """ self.url = api_url.rstrip('/') + self.timeout = timeout def _get(self, path, params=None): """Perform get request""" - r = requests.get(self.url + path, params=params, timeout=30) + r = requests.get(self.url + path, params=params, timeout=self.timeout) # r.raise_for_status() return r.json() From 07af33509c1dc6f9b3c8af649d38c3229a778f50 Mon Sep 17 00:00:00 2001 From: ankit1200 Date: Fri, 8 Dec 2017 22:33:31 -0500 Subject: [PATCH 089/174] self.ws.send doesn't need to happen twice --- gdax/websocket_client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index 0c679e10..635c3ffd 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -66,7 +66,6 @@ def _connect(self): sub_params.update(get_auth_headers(timestamp, message, self.api_key, self.api_secret, self.api_passphrase)) self.ws = create_connection(self.url) - self.ws.send(json.dumps(sub_params)) if self.type == "heartbeat": sub_params = {"type": "heartbeat", "on": True} From f1bdf462b30fc2b080fe95c71b9ee6355f75b544 Mon Sep 17 00:00:00 2001 From: Skylar Saveland Date: Wed, 13 Dec 2017 12:17:19 -0800 Subject: [PATCH 090/174] #180 update authenticated_client --- gdax/authenticated_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index 5128a0ad..f01ca8a2 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -130,7 +130,7 @@ def paginate_orders(self, product_id, status, result, after): if r.json(): result.append(r.json()) if 'cb-after' in r.headers: - self.paginate_orders(product_id, result, r.headers['cb-after']) + self.paginate_orders(product_id, status, result, r.headers['cb-after']) return result def get_fills(self, order_id='', product_id='', before='', after='', limit=''): From 14a88c3a771fa96c8339d95db1dcb2b8d9806cf0 Mon Sep 17 00:00:00 2001 From: Myxomatosis Date: Sun, 17 Dec 2017 13:50:29 -0800 Subject: [PATCH 091/174] Fixed websocket authentication Changed lines 64-73 to get websocket authentication working. --- gdax/websocket_client.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index de468e7e..96382cd4 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -62,8 +62,15 @@ def _connect(self): if self.auth: timestamp = str(time.time()) - message = timestamp + 'GET' + '/users/self' - sub_params.update(get_auth_headers(timestamp, message, self.api_key, self.api_secret, self.api_passphrase)) + message = timestamp + 'GET' + '/users/self/verify' + message = message.encode('ascii') + hmac_key = base64.b64decode(self.api_secret) + signature = hmac.new(hmac_key, message, hashlib.sha256) + signature_b64 = signature.digest().encode('base64').rstrip('\n') + sub_params['signature'] = signature_b64 + sub_params['key'] = self.api_key + sub_params['passphrase'] = self.api_passphrase + sub_params['timestamp'] = timestamp self.ws = create_connection(self.url) self.ws.send(json.dumps(sub_params)) From 9970aee5edde9bbfe3388fc3be947dc73c5ffb11 Mon Sep 17 00:00:00 2001 From: alistair Date: Sun, 24 Dec 2017 17:26:58 +0000 Subject: [PATCH 092/174] Update as per documentation - https://docs.gdax.com/?python#coinbase54, issue #204 --- gdax/authenticated_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index bd1fe948..0a4f5d84 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -252,7 +252,7 @@ def coinbase_withdraw(self, amount="", currency="", coinbase_account_id=""): "currency": currency, "coinbase_account_id": coinbase_account_id } - r = requests.post(self.url + "/withdrawals/coinbase", data=json.dumps(payload), auth=self.auth, timeout=self.timeout) + r = requests.post(self.url + "/withdrawals/coinbase-account", data=json.dumps(payload), auth=self.auth, timeout=self.timeout) # r.raise_for_status() return r.json() From 9f7e5020fc0d85ed667ba91f0a773a6104068f63 Mon Sep 17 00:00:00 2001 From: alistair Date: Sun, 24 Dec 2017 17:27:37 +0000 Subject: [PATCH 093/174] Make pep8 compliant --- gdax/gdax_auth.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gdax/gdax_auth.py b/gdax/gdax_auth.py index 14757c3c..c4217331 100644 --- a/gdax/gdax_auth.py +++ b/gdax/gdax_auth.py @@ -14,8 +14,11 @@ def __init__(self, api_key, secret_key, passphrase): def __call__(self, request): timestamp = str(time.time()) - message = timestamp + request.method + request.path_url + (request.body or '') - request.headers.update(get_auth_headers(timestamp, message, self.api_key, self.secret_key, + message = ''.join([timestamp, request.method, + request.path_url, (request.body or '')]) + request.headers.update(get_auth_headers(timestamp, message, + self.api_key, + self.secret_key, self.passphrase)) return request From ff07566a0d381d8ee043e597f3adaf432eaae741 Mon Sep 17 00:00:00 2001 From: alistair Date: Mon, 25 Dec 2017 02:38:52 +0000 Subject: [PATCH 094/174] Fix unit test as per issue #203 --- tests/test_public_client.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_public_client.py b/tests/test_public_client.py index d33a1844..9f1ad785 100644 --- a/tests/test_public_client.py +++ b/tests/test_public_client.py @@ -1,6 +1,8 @@ import pytest import gdax import time +import datetime +from dateutil.relativedelta import relativedelta @pytest.fixture(scope='module') @@ -46,9 +48,11 @@ def test_get_product_trades(self, client): assert type(r) is list assert 'trade_id' in r[0] - @pytest.mark.parametrize('start', ('2017-11-01', None)) - @pytest.mark.parametrize('end', ('2017-11-30', None)) - @pytest.mark.parametrize('granularity', (3600, None)) + current_time = datetime.datetime.now() + + @pytest.mark.parametrize('start,end,granularity', + [(current_time - relativedelta(months=1), + current_time, 10000)]) def test_get_historic_rates(self, client, start, end, granularity): r = client.get_product_historic_rates('BTC-USD', start=start, end=end, granularity=granularity) assert type(r) is list From ed6a3e00c3de8baef7df5a18b4a483a64c4f280e Mon Sep 17 00:00:00 2001 From: jlas Date: Sat, 30 Dec 2017 18:21:18 +0000 Subject: [PATCH 095/174] Send ping based on time elapsed, modulus op needs to line up exactly --- gdax/websocket_client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index 96382cd4..5f5ec020 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -84,9 +84,11 @@ def _connect(self): def _listen(self): while not self.stop: try: - if int(time.time() % 30) == 0: + start_t = 0 + if time.time() - start_t >= 30: # Set a 30 second ping to keep connection alive self.ws.ping("keepalive") + start_t = time.time() data = self.ws.recv() msg = json.loads(data) except ValueError as e: From 5e0bc6be2cd312879d51879f052de15917ad4c44 Mon Sep 17 00:00:00 2001 From: Keith Smith Date: Mon, 1 Jan 2018 15:42:15 -0600 Subject: [PATCH 096/174] Generate GDAX Deposit Address --- gdax/authenticated_client.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index bd1fe948..6fa125cd 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -300,3 +300,8 @@ def get_trailing_volume(self): r = requests.get(self.url + "/users/self/trailing-volume", auth=self.auth, timeout=self.timeout) # r.raise_for_status() return r.json() + + def get_deposit_address(self, account_id): + r = requests.post(self.url + '/coinbase-accounts/{}/addresses'.format(account_id), auth=self.auth, timeout=self.timeout) + # r.raise_for_status() + return r.json() From 05ea2886dcd4f1078a67cb1275dd97351d9ee2b4 Mon Sep 17 00:00:00 2001 From: Sebastian Quilter Date: Wed, 3 Jan 2018 11:27:45 -0700 Subject: [PATCH 097/174] update websocket_client to support python3 Fixes #218 --- gdax/websocket_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index 96382cd4..747f23d7 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -66,7 +66,7 @@ def _connect(self): message = message.encode('ascii') hmac_key = base64.b64decode(self.api_secret) signature = hmac.new(hmac_key, message, hashlib.sha256) - signature_b64 = signature.digest().encode('base64').rstrip('\n') + signature_b64 = base64.b64encode(signature.digest()).decode('utf-8').rstrip('\n') sub_params['signature'] = signature_b64 sub_params['key'] = self.api_key sub_params['passphrase'] = self.api_passphrase From dd236f8f421ebcce86eea70699c64cc04b2811b2 Mon Sep 17 00:00:00 2001 From: Dave Date: Sun, 7 Jan 2018 20:59:03 -0500 Subject: [PATCH 098/174] Catches unaccepted granularity levels in PublicClient.get_product_historic_rates(), and uses the nearest accepted level. --- gdax/public_client.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gdax/public_client.py b/gdax/public_client.py index 79fef798..43e769d2 100644 --- a/gdax/public_client.py +++ b/gdax/public_client.py @@ -187,6 +187,11 @@ def get_product_historic_rates(self, product_id, start=None, end=None, if end is not None: params['end'] = end if granularity is not None: + acceptedGrans = [60, 300, 900, 3600, 21600, 86400] + if granularity not in acceptedGrans: + newGranularity = min(acceptedGrans, key=lambda x:abs(x-granularity)) + print(granularity,' is not a valid granularity level, using',newGranularity,' instead.') + granularity = newGranularity params['granularity'] = granularity return self._get('/products/{}/candles'.format(str(product_id)), params=params) From 127cc3c09e6adb0204da8d4cdc743f42b80bdce2 Mon Sep 17 00:00:00 2001 From: Chase Shimmin Date: Thu, 18 Jan 2018 08:33:32 -0500 Subject: [PATCH 099/174] Fix bug in AuthenticatedClient.cancel_all() Fixed bug where `cancel_all()` deletes all orders, even if `product_id` argument is specified. --- gdax/authenticated_client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py index bd1fe948..b71a241d 100644 --- a/gdax/authenticated_client.py +++ b/gdax/authenticated_client.py @@ -90,9 +90,10 @@ def cancel_order(self, order_id): def cancel_all(self, product_id=''): url = self.url + '/orders/' + params = {} if product_id: - url += "?product_id={}&".format(str(product_id)) - r = requests.delete(url, auth=self.auth, timeout=self.timeout) + params["product_id"] = product_id + r = requests.delete(url, auth=self.auth, params=params, timeout=self.timeout) # r.raise_for_status() return r.json() From cf98e87657f703064eca0ecca282f54c178fcdeb Mon Sep 17 00:00:00 2001 From: Todd Sharpe Date: Sat, 20 Jan 2018 01:03:24 -0500 Subject: [PATCH 100/174] Adding .cache/ and .coverage to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index cb98d5cc..60adeb5b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ dist/ venv/ *log.txt gdax/__pycache__/ +.cache/ +.coverage +tests/__pycache__/ From 47b216c0cb13345b99a82a652f1f17a7424a17e4 Mon Sep 17 00:00:00 2001 From: TheFrostyboss Date: Sat, 20 Jan 2018 12:41:25 -0700 Subject: [PATCH 101/174] README.md Fixed cancel_all documentation (product => product_id) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 64ea76e6..d199167d 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ auth_client.cancel_order("d50ec984-77a8-460a-b958-66f114b0de9b") ``` - [cancel_all](https://docs.gdax.com/#cancel-all) ```python -auth_client.cancel_all(product='BTC-USD') +auth_client.cancel_all(product_id='BTC-USD') ``` - [get_orders](https://docs.gdax.com/#list-orders) (paginated) From a7ca57021ab9024f644e1be2e8786abe7f1b80b4 Mon Sep 17 00:00:00 2001 From: Cabi Date: Sun, 11 Feb 2018 16:37:03 +0000 Subject: [PATCH 102/174] Add Dockerfile --- Dockerfile | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..10de529b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +# Docker environment for ubuntu, conda, python3.6 +# +# Usage: +# * build the image: +# gdax-python$ docker build -t gdax-python . +# * start the image: +# docker run -it gdax-python + +# Latest version of ubuntu +FROM ubuntu:16.04 + +# Install system packages +RUN apt-get update && \ + apt-get install -y wget git libhdf5-dev g++ graphviz openmpi-bin libgl1-mesa-glx bzip2 + +# Install conda +ENV CONDA_DIR /opt/conda +ENV PATH $CONDA_DIR/bin:$PATH + +RUN wget --quiet https://repo.continuum.io/miniconda/Miniconda3-4.2.12-Linux-x86_64.sh && \ + echo "c59b3dd3cad550ac7596e0d599b91e75d88826db132e4146030ef471bb434e9a *Miniconda3-4.2.12-Linux-x86_64.sh" | sha256sum -c - && \ + /bin/bash /Miniconda3-4.2.12-Linux-x86_64.sh -f -b -p $CONDA_DIR && \ + rm Miniconda3-4.2.12-Linux-x86_64.sh && \ + echo export PATH=$CONDA_DIR/bin:'$PATH' > /etc/profile.d/conda.sh + +# Install Python packages +ARG python_version=3.6 + +RUN conda install -y python=${python_version} && \ + pip install --upgrade pip + +# Set gdax-python code path +ENV CODE_DIR /code/gdax-python + +RUN mkdir -p $CODE_DIR +COPY . $CODE_DIR + +RUN cd $CODE_DIR && \ + pip install gdax From aa0b9d7cdda4b67936da7689ed5df7589c92f22f Mon Sep 17 00:00:00 2001 From: Gabriel Smadi Date: Sat, 17 Feb 2018 22:23:04 -0500 Subject: [PATCH 103/174] Bump version pymongo==3.6.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2ef54de0..4a4d5dd8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ bintrees==2.0.7 requests==2.13.0 six==1.10.0 websocket-client==0.40.0 -pymongo==3.5.1 \ No newline at end of file +pymongo==3.6.0 \ No newline at end of file From 17f2d512acd48cf2ab4c4c185af83a99ed02be17 Mon Sep 17 00:00:00 2001 From: Gabriel Smadi Date: Thu, 22 Feb 2018 22:00:32 -0500 Subject: [PATCH 104/174] Fix heartbeat subscribe failure of first message --- gdax/order_book.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gdax/order_book.py b/gdax/order_book.py index 6af28f0c..b707c911 100644 --- a/gdax/order_book.py +++ b/gdax/order_book.py @@ -60,7 +60,7 @@ def on_message(self, message): if self._log_to: pickle.dump(message, self._log_to) - sequence = message['sequence'] + sequence = message.get('sequence', -1) if self._sequence == -1: self.reset_book() return From 5d19e372daa621ec8e29f64569634b64ee4f72d4 Mon Sep 17 00:00:00 2001 From: Gabriel Smadi Date: Thu, 22 Feb 2018 22:07:54 -0500 Subject: [PATCH 105/174] Revert pymongo version for unneeded upgrade --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4a4d5dd8..2ef54de0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ bintrees==2.0.7 requests==2.13.0 six==1.10.0 websocket-client==0.40.0 -pymongo==3.6.0 \ No newline at end of file +pymongo==3.5.1 \ No newline at end of file From 267bdd102b570b22d1627d94070540bf17451bda Mon Sep 17 00:00:00 2001 From: Nicolas Steven Miller Date: Sun, 25 Feb 2018 14:46:45 -0800 Subject: [PATCH 106/174] fix websocket client heartbeat startup failure --- gdax/websocket_client.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py index 762f6b1c..47000b12 100644 --- a/gdax/websocket_client.py +++ b/gdax/websocket_client.py @@ -74,10 +74,6 @@ def _connect(self): self.ws = create_connection(self.url) - if self.type == "heartbeat": - sub_params = {"type": "heartbeat", "on": True} - else: - sub_params = {"type": "heartbeat", "on": False} self.ws.send(json.dumps(sub_params)) def _listen(self): @@ -98,8 +94,6 @@ def _listen(self): self.on_message(msg) def _disconnect(self): - if self.type == "heartbeat": - self.ws.send(json.dumps({"type": "heartbeat", "on": False})) try: if self.ws: self.ws.close() From f1a505ddc56be6c341b1de355b7aaa324d614fd7 Mon Sep 17 00:00:00 2001 From: Brian Boonstra Date: Sun, 11 Mar 2018 14:10:09 -0500 Subject: [PATCH 107/174] Test for level 3 fixes bug with 2 tests for level 2 --- tests/test_public_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_public_client.py b/tests/test_public_client.py index 9f1ad785..4e9fd441 100644 --- a/tests/test_public_client.py +++ b/tests/test_public_client.py @@ -34,7 +34,7 @@ def test_get_product_order_book(self, client, level): if level is 2 and (len(r['asks']) > 50 or len(r['bids']) > 50): pytest.fail('Fail: Level 2 should only return the top 50 asks and bids') - if level is 2 and (len(r['asks']) < 50 or len(r['bids']) < 50): + if level is 3 and (len(r['asks']) < 50 or len(r['bids']) < 50): pytest.fail('Fail: Level 3 should return the full order book') def test_get_product_ticker(self, client): From 1ad117dcd22f05667300810905301d3deb3ea5e1 Mon Sep 17 00:00:00 2001 From: Brian Boonstra Date: Sun, 11 Mar 2018 14:12:05 -0500 Subject: [PATCH 108/174] Replace bintree.RBTree (deprecated by its author) with faster sortedcontainers.SortedDict (recommended by bintree authors) in order book object. Performance testing indicates a few percent faster now. --- gdax/order_book.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/gdax/order_book.py b/gdax/order_book.py index 6af28f0c..3c4fc2eb 100644 --- a/gdax/order_book.py +++ b/gdax/order_book.py @@ -4,7 +4,7 @@ # # Live order book updated from the gdax Websocket Feed -from bintrees import RBTree +from sortedcontainers import SortedDict from decimal import Decimal import pickle @@ -15,8 +15,8 @@ class OrderBook(WebsocketClient): def __init__(self, product_id='BTC-USD', log_to=None): super(OrderBook, self).__init__(products=product_id) - self._asks = RBTree() - self._bids = RBTree() + self._asks = SortedDict() + self._bids = SortedDict() self._client = PublicClient() self._sequence = -1 self._log_to = log_to @@ -37,8 +37,8 @@ def on_close(self): print("\n-- OrderBook Socket Closed! --") def reset_book(self): - self._asks = RBTree() - self._bids = RBTree() + self._asks = SortedDict() + self._bids = SortedDict() res = self._client.get_product_order_book(product_id=self.product_id, level=3) for bid in res['bids']: self.add({ @@ -219,28 +219,28 @@ def get_current_book(self): return result def get_ask(self): - return self._asks.min_key() + return self._asks.peekitem(0) def get_asks(self, price): return self._asks.get(price) def remove_asks(self, price): - self._asks.remove(price) + del self._asks[price] def set_asks(self, price, asks): - self._asks.insert(price, asks) + self._asks[price] = asks def get_bid(self): - return self._bids.max_key() + return self._bids.peekitem(-1) def get_bids(self, price): return self._bids.get(price) def remove_bids(self, price): - self._bids.remove(price) + del self._bids[price] def set_bids(self, price, bids): - self._bids.insert(price, bids) + self._bids[price] = bids if __name__ == '__main__': From 68f0e9018ab85298820b3c6e4671bf7deaa52988 Mon Sep 17 00:00:00 2001 From: Brian Boonstra Date: Sun, 11 Mar 2018 14:15:08 -0500 Subject: [PATCH 109/174] Replace bintree.RBTree (deprecated by its author) with faster sortedcontainers.SortedDict (recommended by bintree authors) in order book object. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f7f58718..8a53a75c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -bintrees==2.0.7 +sortedcontainers>=1.5.9 requests==2.13.0 six==1.10.0 websocket-client==0.40.0 From 308d59a6f10c3265eab79e9227a4b2a348777720 Mon Sep 17 00:00:00 2001 From: Brian Boonstra Date: Tue, 13 Mar 2018 20:41:35 -0500 Subject: [PATCH 110/174] Restore top of book to previous single price behavior --- gdax/order_book.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gdax/order_book.py b/gdax/order_book.py index 3c4fc2eb..80d4a610 100644 --- a/gdax/order_book.py +++ b/gdax/order_book.py @@ -219,7 +219,7 @@ def get_current_book(self): return result def get_ask(self): - return self._asks.peekitem(0) + return self._asks.peekitem(0)[0] def get_asks(self, price): return self._asks.get(price) @@ -231,7 +231,7 @@ def set_asks(self, price, asks): self._asks[price] = asks def get_bid(self): - return self._bids.peekitem(-1) + return self._bids.peekitem(-1)[0] def get_bids(self, price): return self._bids.get(price) From da5c38bc14edd8973bb364361c07631e382512fe Mon Sep 17 00:00:00 2001 From: Tamer Saadeh Date: Fri, 23 Mar 2018 17:40:30 +0100 Subject: [PATCH 111/174] Allow get_product_trades to be paginated This implements the pagination of trades, as per the docs (https://docs.gdax.com/#get-trades). --- gdax/public_client.py | 47 ++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/gdax/public_client.py b/gdax/public_client.py index 43e769d2..3ce4fd29 100644 --- a/gdax/public_client.py +++ b/gdax/public_client.py @@ -119,30 +119,35 @@ def get_product_ticker(self, product_id): """ return self._get('/products/{}/ticker'.format(str(product_id))) - def get_product_trades(self, product_id): - """List the latest trades for a product. + def get_product_trades(self, product_id, before='', after='', limit='', result=[]): + url = self.url + '/products/{}/trades'.format(str(product_id)) + params = {} - Args: - product_id (str): Product + if before: + params['before'] = str(before) + if after: + params['after'] = str(after) + if limit and limit < 100: + # the default limit is 100 + # we only add it if the limit is less than 100 + params['limit'] = limit - Returns: - list: Latest trades. Example:: - [{ - "time": "2014-11-07T22:19:28.578544Z", - "trade_id": 74, - "price": "10.00000000", - "size": "0.01000000", - "side": "buy" - }, { - "time": "2014-11-07T01:08:43.642366Z", - "trade_id": 73, - "price": "100.00000000", - "size": "0.01000000", - "side": "sell" - }] + r = requests.get(url, params=params) + r.raise_for_status() - """ - return self._get('/products/{}/trades'.format(str(product_id))) + result.extend(r.json()) + + if 'cb-after' in r.headers and limit is not len(result): + # update limit + limit -= len(result) + if limit <= 0: + return result + + # ensure that we don't get rate-limited/blocked + time.sleep(0.4) + return self.get_product_trades(product_id=product_id, after=r.headers['cb-after'], limit=limit, result=result) + + return result def get_product_historic_rates(self, product_id, start=None, end=None, granularity=None): From 23f8949b24773405ce9fffdbe123f7365ee6695b Mon Sep 17 00:00:00 2001 From: Tamer Saadeh Date: Fri, 23 Mar 2018 17:51:04 +0100 Subject: [PATCH 112/174] add docs --- gdax/public_client.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/gdax/public_client.py b/gdax/public_client.py index 3ce4fd29..0c970e5e 100644 --- a/gdax/public_client.py +++ b/gdax/public_client.py @@ -120,6 +120,30 @@ def get_product_ticker(self, product_id): return self._get('/products/{}/ticker'.format(str(product_id))) def get_product_trades(self, product_id, before='', after='', limit='', result=[]): + """List the latest trades for a product. + Args: + product_id (str): Product + before (Optional[str]): start time in ISO 8601 + after (Optional[str]): end time in ISO 8601 + limit (Optional[int]): the desired number of trades (can be more than 100, + automatically paginated) + results (Optional[list]): list of results that is used for the pagination + Returns: + list: Latest trades. Example:: + [{ + "time": "2014-11-07T22:19:28.578544Z", + "trade_id": 74, + "price": "10.00000000", + "size": "0.01000000", + "side": "buy" + }, { + "time": "2014-11-07T01:08:43.642366Z", + "trade_id": 73, + "price": "100.00000000", + "size": "0.01000000", + "side": "sell" + }] + """" url = self.url + '/products/{}/trades'.format(str(product_id)) params = {} From fb2f0d6d0775e341f39ce161efbb920c0684514a Mon Sep 17 00:00:00 2001 From: Tamer Saadeh Date: Sat, 24 Mar 2018 11:41:33 +0100 Subject: [PATCH 113/174] remove raise and comment out time.sleep --- gdax/public_client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gdax/public_client.py b/gdax/public_client.py index 0c970e5e..282021a4 100644 --- a/gdax/public_client.py +++ b/gdax/public_client.py @@ -157,7 +157,7 @@ def get_product_trades(self, product_id, before='', after='', limit='', result=[ params['limit'] = limit r = requests.get(url, params=params) - r.raise_for_status() + # r.raise_for_status() result.extend(r.json()) @@ -167,8 +167,8 @@ def get_product_trades(self, product_id, before='', after='', limit='', result=[ if limit <= 0: return result - # ensure that we don't get rate-limited/blocked - time.sleep(0.4) + # TODO: need a way to ensure that we don't get rate-limited/blocked + # time.sleep(0.4) return self.get_product_trades(product_id=product_id, after=r.headers['cb-after'], limit=limit, result=result) return result From 8f2539bfa50f9c10c095e77ed5cd582402801075 Mon Sep 17 00:00:00 2001 From: Nikalexis Nikos Date: Tue, 3 Apr 2018 19:56:02 +0300 Subject: [PATCH 114/174] Fix SyntaxError: EOL while scanning string literal --- gdax/public_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gdax/public_client.py b/gdax/public_client.py index 282021a4..a998873a 100644 --- a/gdax/public_client.py +++ b/gdax/public_client.py @@ -143,7 +143,7 @@ def get_product_trades(self, product_id, before='', after='', limit='', result=[ "size": "0.01000000", "side": "sell" }] - """" + """ url = self.url + '/products/{}/trades'.format(str(product_id)) params = {} From 49613dec472d55a6a3e8bdd845de5d02ec2cb96d Mon Sep 17 00:00:00 2001 From: Aaron Adler Date: Mon, 18 Jun 2018 23:44:22 -0700 Subject: [PATCH 115/174] Add requirement required to run unit tests --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8a53a75c..923b4555 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ six==1.10.0 websocket-client==0.40.0 pymongo==3.5.1 pytest>=3.3.0 -pytest-cov>=2.5.0 \ No newline at end of file +pytest-cov>=2.5.0 +py-dateutil==2.2 From 6ba95d921dbc4e805384e5fd74bc4800548bc83a Mon Sep 17 00:00:00 2001 From: Aaron Adler Date: Mon, 18 Jun 2018 23:50:22 -0700 Subject: [PATCH 116/174] Replace string default for int argument with None Unit test for `get_product_trades` now passes --- gdax/public_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gdax/public_client.py b/gdax/public_client.py index a998873a..24dbb4ec 100644 --- a/gdax/public_client.py +++ b/gdax/public_client.py @@ -119,7 +119,7 @@ def get_product_ticker(self, product_id): """ return self._get('/products/{}/ticker'.format(str(product_id))) - def get_product_trades(self, product_id, before='', after='', limit='', result=[]): + def get_product_trades(self, product_id, before='', after='', limit=None, result=[]): """List the latest trades for a product. Args: product_id (str): Product @@ -161,7 +161,7 @@ def get_product_trades(self, product_id, before='', after='', limit='', result=[ result.extend(r.json()) - if 'cb-after' in r.headers and limit is not len(result): + if 'cb-after' in r.headers and limit is not len(result) and limit is not None: # update limit limit -= len(result) if limit <= 0: From 5ea926ec69587ee77cfb541b70a26c6e98484758 Mon Sep 17 00:00:00 2001 From: Aaron Adler Date: Mon, 18 Jun 2018 23:50:32 -0700 Subject: [PATCH 117/174] Ignore test outputs --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 60adeb5b..7dcfef5a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ gdax/__pycache__/ .cache/ .coverage tests/__pycache__/ +.pytest_cache From a51463f4915cf3743ec12e57e489f39999ecc83f Mon Sep 17 00:00:00 2001 From: Aaron Adler Date: Mon, 18 Jun 2018 23:53:46 -0700 Subject: [PATCH 118/174] Replace named parameter with mutable argument In Python, named parameters that are set to default to mutable arguments retain changes to that argument across function calls http://effbot.org/zone/default-values.htm --- gdax/public_client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gdax/public_client.py b/gdax/public_client.py index 24dbb4ec..0a4e4182 100644 --- a/gdax/public_client.py +++ b/gdax/public_client.py @@ -119,7 +119,7 @@ def get_product_ticker(self, product_id): """ return self._get('/products/{}/ticker'.format(str(product_id))) - def get_product_trades(self, product_id, before='', after='', limit=None, result=[]): + def get_product_trades(self, product_id, before='', after='', limit=None, result=None): """List the latest trades for a product. Args: product_id (str): Product @@ -144,6 +144,9 @@ def get_product_trades(self, product_id, before='', after='', limit=None, result "side": "sell" }] """ + if result is None: + result = [] + url = self.url + '/products/{}/trades'.format(str(product_id)) params = {} From 90e5c62887162013da8c160d6e6775c2b4dc225e Mon Sep 17 00:00:00 2001 From: Aaron Adler Date: Tue, 19 Jun 2018 00:20:11 -0700 Subject: [PATCH 119/174] Switch granularity to only use approved values or throw ValueError --- gdax/public_client.py | 10 +++++----- tests/test_public_client.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/gdax/public_client.py b/gdax/public_client.py index 0a4e4182..656b2ff8 100644 --- a/gdax/public_client.py +++ b/gdax/public_client.py @@ -202,10 +202,10 @@ def get_product_historic_rates(self, product_id, start=None, end=None, product_id (str): Product start (Optional[str]): Start time in ISO 8601 end (Optional[str]): End time in ISO 8601 - granularity (Optional[str]): Desired time slice in seconds + granularity (Optional[int]): Desired time slice in seconds Returns: - list: Historic candle data. Example:: + list: Historic candle data. Example: [ [ time, low, high, open, close, volume ], [ 1415398768, 0.32, 4.2, 0.35, 4.2, 12.3 ], @@ -221,9 +221,9 @@ def get_product_historic_rates(self, product_id, start=None, end=None, if granularity is not None: acceptedGrans = [60, 300, 900, 3600, 21600, 86400] if granularity not in acceptedGrans: - newGranularity = min(acceptedGrans, key=lambda x:abs(x-granularity)) - print(granularity,' is not a valid granularity level, using',newGranularity,' instead.') - granularity = newGranularity + raise ValueError( 'Specified granularity is {}, must be in approved values: {}'.format( + granularity, acceptedGrans) ) + params['granularity'] = granularity return self._get('/products/{}/candles'.format(str(product_id)), params=params) diff --git a/tests/test_public_client.py b/tests/test_public_client.py index 4e9fd441..c70a7f70 100644 --- a/tests/test_public_client.py +++ b/tests/test_public_client.py @@ -52,7 +52,7 @@ def test_get_product_trades(self, client): @pytest.mark.parametrize('start,end,granularity', [(current_time - relativedelta(months=1), - current_time, 10000)]) + current_time, 21600)]) def test_get_historic_rates(self, client, start, end, granularity): r = client.get_product_historic_rates('BTC-USD', start=start, end=end, granularity=granularity) assert type(r) is list From 58a75c82c47ab702b13faf316a7f9cd901c33ccd Mon Sep 17 00:00:00 2001 From: Aaron Adler Date: Tue, 19 Jun 2018 00:35:47 -0700 Subject: [PATCH 120/174] Add better unit test case --- tests/test_public_client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_public_client.py b/tests/test_public_client.py index c70a7f70..8064591e 100644 --- a/tests/test_public_client.py +++ b/tests/test_public_client.py @@ -56,6 +56,8 @@ def test_get_product_trades(self, client): def test_get_historic_rates(self, client, start, end, granularity): r = client.get_product_historic_rates('BTC-USD', start=start, end=end, granularity=granularity) assert type(r) is list + for ticker in r: + assert( all( [type(x) in (int, float) for x in ticker ] ) ) def test_get_product_24hr_stats(self, client): r = client.get_product_24hr_stats('BTC-USD') From 8a82b8010a0f07ff7adb74d70b9ad265f1ab17de Mon Sep 17 00:00:00 2001 From: maxl11 Date: Fri, 6 Jul 2018 15:00:44 +0200 Subject: [PATCH 121/174] update of api-url --- gdax/public_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gdax/public_client.py b/gdax/public_client.py index a998873a..5641b65d 100644 --- a/gdax/public_client.py +++ b/gdax/public_client.py @@ -18,7 +18,7 @@ class PublicClient(object): """ - def __init__(self, api_url='https://api.gdax.com', timeout=30): + def __init__(self, api_url='https://api.pro.coinbase.com', timeout=30): """Create GDAX API public client. Args: From 4a3aadbf5d0e8df8418c581a6fa23dadb849842e Mon Sep 17 00:00:00 2001 From: Tim Paine Date: Fri, 13 Jul 2018 09:56:20 -0400 Subject: [PATCH 122/174] outdated dependency in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 368ee131..15a83d03 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup, find_packages install_requires = [ - 'bintrees==2.0.7', + 'sortedcontainers>=1.5.9', 'requests==2.13.0', 'six==1.10.0', 'websocket-client==0.40.0', From 54b7b05db759d98d8fb6f929644b2d489befd420 Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Mon, 13 Aug 2018 09:44:52 -0400 Subject: [PATCH 123/174] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d199167d..34af692f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# gdax-python -The Python client for the [GDAX API](https://docs.gdax.com/) (formerly known as -the Coinbase Exchange API) +# coinbasepro-python +The Python client for the [Coinbase Pro API](https://docs.pro.coinbase.com/) (formerly known as +the GDAX) ##### Provided under MIT License by Daniel Paquin. *Note: this library may be subtly broken or buggy. The code is released under From ced08c654b1638af1d879d1ec06c5d3505b920d4 Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Sun, 19 Aug 2018 19:08:30 -0400 Subject: [PATCH 124/174] Release of cbpro 1.1.0 --- Dockerfile | 10 +- README.md | 103 ++-- cbpro/.gitignore | 2 + cbpro/__init__.py | 5 + cbpro/authenticated_client.py | 995 ++++++++++++++++++++++++++++++++++ cbpro/cbpro_auth.py | 37 ++ cbpro/order_book.py | 298 ++++++++++ cbpro/public_client.py | 310 +++++++++++ cbpro/websocket_client.py | 160 ++++++ pytest.ini | 2 +- setup.py | 18 +- 11 files changed, 1879 insertions(+), 61 deletions(-) create mode 100644 cbpro/.gitignore create mode 100644 cbpro/__init__.py create mode 100644 cbpro/authenticated_client.py create mode 100644 cbpro/cbpro_auth.py create mode 100644 cbpro/order_book.py create mode 100644 cbpro/public_client.py create mode 100644 cbpro/websocket_client.py diff --git a/Dockerfile b/Dockerfile index 10de529b..d6285fd5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,9 +2,9 @@ # # Usage: # * build the image: -# gdax-python$ docker build -t gdax-python . +# coinbasepro-python$ docker build -t coinbasepro-python . # * start the image: -# docker run -it gdax-python +# docker run -it coinbasepro-python # Latest version of ubuntu FROM ubuntu:16.04 @@ -29,11 +29,11 @@ ARG python_version=3.6 RUN conda install -y python=${python_version} && \ pip install --upgrade pip -# Set gdax-python code path -ENV CODE_DIR /code/gdax-python +# Set coinbasepro-python code path +ENV CODE_DIR /code/coinbasepro-python RUN mkdir -p $CODE_DIR COPY . $CODE_DIR RUN cd $CODE_DIR && \ - pip install gdax + pip install cbpro diff --git a/README.md b/README.md index 441bbc7a..77c26e89 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. largest Bitcoin exchanges in the *world*! - Do not worry about handling the nuances of the API with easy-to-use methods for every API endpoint. -- Gain an advantage in the market by getting under the hood of GDAX to learn -what and who is *really* behind every tick. +- Gain an advantage in the market by getting under the hood of CB Pro to learn +what and who is behind every tick. ## Under Development - Test Scripts @@ -29,15 +29,17 @@ what and who is *really* behind every tick. ## Getting Started This README is documentation on the syntax of the python client presented in this repository. See function docstrings for full syntax details. -**This API attempts to present a clean interface to GDAX, but in order to use it -to its full potential, you must familiarize yourself with the official GDAX +**This API attempts to present a clean interface to CB Pro, but in order to use it +to its full potential, you must familiarize yourself with the official CB Pro documentation.** -- https://docs.gdax.com/ +- https://docs.pro.coinbase.com/ - You may manually install the project or use ```pip```: ```python -pip install gdax +pip install cbpro +#or +pip install git+git://github.com/danpaquin/coinbasepro-python.git ``` ### Public Client @@ -45,17 +47,17 @@ Only some endpoints in the API are available to everyone. The public endpoints can be reached using ```PublicClient``` ```python -import gdax -public_client = gdax.PublicClient() +import cbpro +public_client = cbpro.PublicClient() ``` ### PublicClient Methods -- [get_products](https://docs.gdax.com/#get-products) +- [get_products](https://docs.pro.coinbase.com//#get-products) ```python public_client.get_products() ``` -- [get_product_order_book](https://docs.gdax.com/#get-product-order-book) +- [get_product_order_book](https://docs.pro.coinbase.com/#get-product-order-book) ```python # Get the order book at the default level. public_client.get_product_order_book('BTC-USD') @@ -63,37 +65,37 @@ public_client.get_product_order_book('BTC-USD') public_client.get_product_order_book('BTC-USD', level=1) ``` -- [get_product_ticker](https://docs.gdax.com/#get-product-ticker) +- [get_product_ticker](https://docs.pro.coinbase.com/#get-product-ticker) ```python # Get the product ticker for a specific product. public_client.get_product_ticker(product_id='ETH-USD') ``` -- [get_product_trades](https://docs.gdax.com/#get-trades) (paginated) +- [get_product_trades](https://docs.pro.coinbase.com/#get-trades) (paginated) ```python # Get the product trades for a specific product. # Returns a generator public_client.get_product_trades(product_id='ETH-USD') ``` -- [get_product_historic_rates](https://docs.gdax.com/#get-historic-rates) +- [get_product_historic_rates](https://docs.pro.coinbase.com/#get-historic-rates) ```python public_client.get_product_historic_rates('ETH-USD') # To include other parameters, see function docstring: public_client.get_product_historic_rates('ETH-USD', granularity=3000) ``` -- [get_product_24hr_stats](https://docs.gdax.com/#get-24hr-stats) +- [get_product_24hr_stats](https://docs.pro.coinbase.com/#get-24hr-stats) ```python public_client.get_product_24hr_stats('ETH-USD') ``` -- [get_currencies](https://docs.gdax.com/#get-currencies) +- [get_currencies](https://docs.pro.coinbase.com/#get-currencies) ```python public_client.get_currencies() ``` -- [get_time](https://docs.gdax.com/#time) +- [get_time](https://docs.pro.coinbase.com/#time) ```python public_client.get_time() ``` @@ -103,22 +105,22 @@ public_client.get_time() Not all API endpoints are available to everyone. Those requiring user authentication can be reached using `AuthenticatedClient`. You must setup API access within your -[account settings](https://www.gdax.com/settings/api). +[account settings](https://www.pro.coinbase.com/settings/api). The `AuthenticatedClient` inherits all methods from the `PublicClient` class, so you will only need to initialize one if you are planning to integrate both into your script. ```python -import gdax -auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase) +import cbpro +auth_client = cbpro.AuthenticatedClient(key, b64secret, passphrase) # Use the sandbox API (requires a different set of API access credentials) -auth_client = gdax.AuthenticatedClient(key, b64secret, passphrase, - api_url="https://api-public.sandbox.gdax.com") +auth_client = cbpro.AuthenticatedClient(key, b64secret, passphrase, + api_url="https://api-public.sandbox.pro.coinbase.com") ``` ### Pagination -Some calls are [paginated](https://docs.gdax.com/#pagination), meaning multiple -calls must be made to receive the full set of data. The GDAX Python API provides +Some calls are [paginated](https://docs.pro.coinbase.com/#pagination), meaning multiple +calls must be made to receive the full set of data. The CB Pro Python API provides an abstraction for paginated endpoints in the form of generators which provide a clean interface for iteration but may make multiple HTTP requests behind the scenes. The pagination options `before`, `after`, and `limit` may be supplied as @@ -133,7 +135,7 @@ new data since the previous request. For the case of `get_fills()`, the `trade_id` is the parameter used for indexing. By passing `before=some_trade_id`, only fills more recent than that `trade_id` will be returned. Note that when using `before`, a maximum of 100 entries will be -returned - this is a limitation of GDAX. +returned - this is a limitation of CB Pro. ```python from itertools import islice # Get 5 most recent fills @@ -143,29 +145,29 @@ new_fills = auth_client.get_fills(before=recent_fills[0]['trade_id']) ``` ### AuthenticatedClient Methods -- [get_accounts](https://docs.gdax.com/#list-accounts) +- [get_accounts](https://docs.pro.coinbase.com/#list-accounts) ```python auth_client.get_accounts() ``` -- [get_account](https://docs.gdax.com/#get-an-account) +- [get_account](https://docs.pro.coinbase.com/#get-an-account) ```python auth_client.get_account("7d0f7d8e-dd34-4d9c-a846-06f431c381ba") ``` -- [get_account_history](https://docs.gdax.com/#get-account-history) (paginated) +- [get_account_history](https://docs.pro.coinbase.com/#get-account-history) (paginated) ```python # Returns generator: auth_client.get_account_history("7d0f7d8e-dd34-4d9c-a846-06f431c381ba") ``` -- [get_account_holds](https://docs.gdax.com/#get-holds) (paginated) +- [get_account_holds](https://docs.pro.coinbase.com/#get-holds) (paginated) ```python # Returns generator: auth_client.get_account_holds("7d0f7d8e-dd34-4d9c-a846-06f431c381ba") ``` -- [buy & sell](https://docs.gdax.com/#place-a-new-order) +- [buy & sell](https://docs.pro.coinbase.com/#place-a-new-order) ```python # Buy 0.01 BTC @ 100 USD auth_client.buy(price='100.00', #USD @@ -202,27 +204,27 @@ auth_client.place_stop_order(product_id='BTC-USD', size='0.01') ``` -- [cancel_order](https://docs.gdax.com/#cancel-an-order) +- [cancel_order](https://docs.pro.coinbase.com/#cancel-an-order) ```python auth_client.cancel_order("d50ec984-77a8-460a-b958-66f114b0de9b") ``` -- [cancel_all](https://docs.gdax.com/#cancel-all) +- [cancel_all](https://docs.pro.coinbase.com/#cancel-all) ```python auth_client.cancel_all(product_id='BTC-USD') ``` -- [get_orders](https://docs.gdax.com/#list-orders) (paginated) +- [get_orders](https://docs.pro.coinbase.com/#list-orders) (paginated) ```python # Returns generator: auth_client.get_orders() ``` -- [get_order](https://docs.gdax.com/#get-an-order) +- [get_order](https://docs.pro.coinbase.com/#get-an-order) ```python auth_client.get_order("d50ec984-77a8-460a-b958-66f114b0de9b") ``` -- [get_fills](https://docs.gdax.com/#list-fills) (paginated) +- [get_fills](https://docs.pro.coinbase.com/#list-fills) (paginated) ```python # All return generators auth_client.get_fills() @@ -232,9 +234,8 @@ auth_client.get_fills(order_id="d50ec984-77a8-460a-b958-66f114b0de9b") auth_client.get_fills(product_id="ETH-BTC") ``` -- [deposit & withdraw](https://docs.gdax.com/#depositwithdraw) +- [deposit & withdraw](https://docs.pro.coinbase.com/#depositwithdraw) ```python -gdax depositParams = { 'amount': '25.00', # Currency determined by account specified 'coinbase_account_id': '60680c98bfe96c2601f27e9c' @@ -242,7 +243,7 @@ depositParams = { auth_client.deposit(depositParams) ``` ```python -# Withdraw from GDAX into Coinbase Wallet +# Withdraw from CB Pro into Coinbase Wallet withdrawParams = { 'amount': '1.00', # Currency determined by account specified 'coinbase_account_id': '536a541fa9393bb3c7000023' @@ -252,22 +253,22 @@ auth_client.withdraw(withdrawParams) ### WebsocketClient If you would like to receive real-time market updates, you must subscribe to the -[websocket feed](https://docs.gdax.com/#websocket-feed). +[websocket feed](https://docs.pro.coinbase.com/#websocket-feed). #### Subscribe to a single product ```python -import gdax +import cbpro # Paramters are optional -wsClient = gdax.WebsocketClient(url="wss://ws-feed.gdax.com", products="BTC-USD") +wsClient = cbpro.WebsocketClient(url="wss://ws-feed.pro.coinbase.com", products="BTC-USD") # Do other stuff... wsClient.close() ``` #### Subscribe to multiple products ```python -import gdax +import cbpro # Paramaters are optional -wsClient = gdax.WebsocketClient(url="wss://ws-feed.gdax.com", +wsClient = cbpro.WebsocketClient(url="wss://ws-feed.pro.coinbase.com", products=["BTC-USD", "ETH-USD"]) # Do other stuff... wsClient.close() @@ -280,7 +281,7 @@ the database collection. ```python # import PyMongo and connect to a local, running Mongo instance from pymongo import MongoClient -import gdax +import cbpro mongo_client = MongoClient('mongodb://localhost:27017/') # specify the database and collection @@ -288,7 +289,7 @@ db = mongo_client.cryptocurrency_database BTC_collection = db.BTC_collection # instantiate a WebsocketClient instance, with a Mongo collection as a parameter -wsClient = gdax.WebsocketClient(url="wss://ws-feed.gdax.com", products="BTC-USD", +wsClient = cbpro.WebsocketClient(url="wss://ws-feed.pro.coinbase.com", products="BTC-USD", mongo_collection=BTC_collection, should_print=False) wsClient.start() ``` @@ -306,10 +307,10 @@ argument that contains the message of dict type. - on_close - called once after the websocket has been closed. - close - call this method to close the websocket connection (do not overwrite). ```python -import gdax, time -class myWebsocketClient(gdax.WebsocketClient): +import cbpro, time +class myWebsocketClient(cbpro.WebsocketClient): def on_open(self): - self.url = "wss://ws-feed.gdax.com/" + self.url = "wss://ws-feed.pro.coinbase.com/" self.products = ["LTC-USD"] self.message_count = 0 print("Lets count the messages!") @@ -344,8 +345,8 @@ the orderbook for the product_id input. Please provide your feedback for future improvements. ```python -import gdax, time -order_book = gdax.OrderBook(product_id='BTC-USD') +import cbpro, time +order_book = cbpro.OrderBook(product_id='BTC-USD') order_book.start() time.sleep(10) order_book.close() @@ -362,6 +363,10 @@ python -m pytest ``` ## Change Log +*1.1* +- Refactor project for Coinbase Pro +- Major overhaul on how pagination is handled + *1.0* **Current PyPI release** - The first release that is not backwards compatible - Refactored to follow PEP 8 Standards diff --git a/cbpro/.gitignore b/cbpro/.gitignore new file mode 100644 index 00000000..79c40e7a --- /dev/null +++ b/cbpro/.gitignore @@ -0,0 +1,2 @@ +*.pyc +FixClient.py \ No newline at end of file diff --git a/cbpro/__init__.py b/cbpro/__init__.py new file mode 100644 index 00000000..00937f59 --- /dev/null +++ b/cbpro/__init__.py @@ -0,0 +1,5 @@ +from cbpro.authenticated_client import AuthenticatedClient +from cbpro.public_client import PublicClient +from cbpro.websocket_client import WebsocketClient +from cbpro.order_book import OrderBook +from cbpro.cbpro_auth import CBProAuth diff --git a/cbpro/authenticated_client.py b/cbpro/authenticated_client.py new file mode 100644 index 00000000..f45f31f5 --- /dev/null +++ b/cbpro/authenticated_client.py @@ -0,0 +1,995 @@ +# +# cbpro/AuthenticatedClient.py +# Daniel Paquin +# +# For authenticated requests to the Coinbase exchange + +import hmac +import hashlib +import time +import requests +import base64 +import json +from requests.auth import AuthBase +from cbpro.public_client import PublicClient +from cbpro.cbpro_auth import CBProAuth + + +class AuthenticatedClient(PublicClient): + """ Provides access to Private Endpoints on the cbpro API. + + All requests default to the live `api_url`: 'https://api.pro.coinbase.com'. + To test your application using the sandbox modify the `api_url`. + + Attributes: + url (str): The api url for this client instance to use. + auth (CBProAuth): Custom authentication handler for each request. + session (requests.Session): Persistent HTTP connection object. + """ + def __init__(self, key, b64secret, passphrase, + api_url="https://api.pro.coinbase.com"): + """ Create an instance of the AuthenticatedClient class. + + Args: + key (str): Your API key. + b64secret (str): The secret key matching your API key. + passphrase (str): Passphrase chosen when setting up key. + api_url (Optional[str]): API URL. Defaults to cbpro API. + """ + super(AuthenticatedClient, self).__init__(api_url) + self.auth = CBProAuth(key, b64secret, passphrase) + self.session = requests.Session() + + def get_account(self, account_id): + """ Get information for a single account. + + Use this endpoint when you know the account_id. + + Args: + account_id (str): Account id for account you want to get. + + Returns: + dict: Account information. Example:: + { + "id": "a1b2c3d4", + "balance": "1.100", + "holds": "0.100", + "available": "1.00", + "currency": "USD" + } + """ + return self._send_message('get', '/accounts/' + account_id) + + def get_accounts(self): + """ Get a list of trading all accounts. + + When you place an order, the funds for the order are placed on + hold. They cannot be used for other orders or withdrawn. Funds + will remain on hold until the order is filled or canceled. The + funds on hold for each account will be specified. + + Returns: + list: Info about all accounts. Example:: + [ + { + "id": "71452118-efc7-4cc4-8780-a5e22d4baa53", + "currency": "BTC", + "balance": "0.0000000000000000", + "available": "0.0000000000000000", + "hold": "0.0000000000000000", + "profile_id": "75da88c5-05bf-4f54-bc85-5c775bd68254" + }, + { + ... + } + ] + + * Additional info included in response for margin accounts. + """ + return self.get_account('') + + def get_account_history(self, account_id, **kwargs): + """ List account activity. Account activity either increases or + decreases your account balance. + + Entry type indicates the reason for the account change. + * transfer: Funds moved to/from Coinbase to cbpro + * match: Funds moved as a result of a trade + * fee: Fee as a result of a trade + * rebate: Fee rebate as per our fee schedule + + If an entry is the result of a trade (match, fee), the details + field will contain additional information about the trade. + + Args: + account_id (str): Account id to get history of. + kwargs (dict): Additional HTTP request parameters. + + Returns: + list: History information for the account. Example:: + [ + { + "id": "100", + "created_at": "2014-11-07T08:19:27.028459Z", + "amount": "0.001", + "balance": "239.669", + "type": "fee", + "details": { + "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", + "trade_id": "74", + "product_id": "BTC-USD" + } + }, + { + ... + } + ] + """ + endpoint = '/accounts/{}/ledger'.format(account_id) + return self._send_paginated_message(endpoint, params=kwargs) + + def get_account_holds(self, account_id, **kwargs): + """ Get holds on an account. + + This method returns a generator which may make multiple HTTP requests + while iterating through it. + + Holds are placed on an account for active orders or + pending withdraw requests. + + As an order is filled, the hold amount is updated. If an order + is canceled, any remaining hold is removed. For a withdraw, once + it is completed, the hold is removed. + + The `type` field will indicate why the hold exists. The hold + type is 'order' for holds related to open orders and 'transfer' + for holds related to a withdraw. + + The `ref` field contains the id of the order or transfer which + created the hold. + + Args: + account_id (str): Account id to get holds of. + kwargs (dict): Additional HTTP request parameters. + + Returns: + generator(list): Hold information for the account. Example:: + [ + { + "id": "82dcd140-c3c7-4507-8de4-2c529cd1a28f", + "account_id": "e0b3f39a-183d-453e-b754-0c13e5bab0b3", + "created_at": "2014-11-06T10:34:47.123456Z", + "updated_at": "2014-11-06T10:40:47.123456Z", + "amount": "4.23", + "type": "order", + "ref": "0a205de4-dd35-4370-a285-fe8fc375a273", + }, + { + ... + } + ] + + """ + endpoint = '/accounts/{}/holds'.format(account_id) + return self._send_paginated_message(endpoint, params=kwargs) + + def place_order(self, product_id, side, order_type, **kwargs): + """ Place an order. + + The three order types (limit, market, and stop) can be placed using this + method. Specific methods are provided for each order type, but if a + more generic interface is desired this method is available. + + Args: + product_id (str): Product to order (eg. 'BTC-USD') + side (str): Order side ('buy' or 'sell) + order_type (str): Order type ('limit', 'market', or 'stop') + **client_oid (str): Order ID selected by you to identify your order. + This should be a UUID, which will be broadcast in the public + feed for `received` messages. + **stp (str): Self-trade prevention flag. cbpro doesn't allow self- + trading. This behavior can be modified with this flag. + Options: + 'dc' Decrease and Cancel (default) + 'co' Cancel oldest + 'cn' Cancel newest + 'cb' Cancel both + **overdraft_enabled (Optional[bool]): If true funding above and + beyond the account balance will be provided by margin, as + necessary. + **funding_amount (Optional[Decimal]): Amount of margin funding to be + provided for the order. Mutually exclusive with + `overdraft_enabled`. + **kwargs: Additional arguments can be specified for different order + types. See the limit/market/stop order methods for details. + + Returns: + dict: Order details. Example:: + { + "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2", + "price": "0.10000000", + "size": "0.01000000", + "product_id": "BTC-USD", + "side": "buy", + "stp": "dc", + "type": "limit", + "time_in_force": "GTC", + "post_only": false, + "created_at": "2016-12-08T20:02:28.53864Z", + "fill_fees": "0.0000000000000000", + "filled_size": "0.00000000", + "executed_value": "0.0000000000000000", + "status": "pending", + "settled": false + } + + """ + # Margin parameter checks + if kwargs.get('overdraft_enabled') is not None and \ + kwargs.get('funding_amount') is not None: + raise ValueError('Margin funding must be specified through use of ' + 'overdraft or by setting a funding amount, but not' + ' both') + + # Limit order checks + if order_type == 'limit': + if kwargs.get('cancel_after') is not None and \ + kwargs.get('tif') != 'GTT': + raise ValueError('May only specify a cancel period when time ' + 'in_force is `GTT`') + if kwargs.get('post_only') is not None and kwargs.get('tif') in \ + ['IOC', 'FOK']: + raise ValueError('post_only is invalid when time in force is ' + '`IOC` or `FOK`') + + # Market and stop order checks + if order_type == 'market' or order_type == 'stop': + if not (kwargs.get('size') is None) ^ (kwargs.get('funds') is None): + raise ValueError('Either `size` or `funds` must be specified ' + 'for market/stop orders (but not both).') + + # Build params dict + params = {'product_id': product_id, + 'side': side, + 'type': order_type} + params.update(kwargs) + return self._send_message('post', '/orders', data=json.dumps(params)) + + def buy(self, product_id, order_type, **kwargs): + """Place a buy order. + + This is included to maintain backwards compatibility with older versions + of cbpro-Python. For maximum support from docstrings and function + signatures see the order type-specific functions place_limit_order, + place_market_order, and place_stop_order. + + Args: + product_id (str): Product to order (eg. 'BTC-USD') + order_type (str): Order type ('limit', 'market', or 'stop') + **kwargs: Additional arguments can be specified for different order + types. + + Returns: + dict: Order details. See `place_order` for example. + + """ + return self.place_order(product_id, 'buy', order_type, **kwargs) + + def sell(self, product_id, order_type, **kwargs): + """Place a sell order. + + This is included to maintain backwards compatibility with older versions + of cbpro-Python. For maximum support from docstrings and function + signatures see the order type-specific functions place_limit_order, + place_market_order, and place_stop_order. + + Args: + product_id (str): Product to order (eg. 'BTC-USD') + order_type (str): Order type ('limit', 'market', or 'stop') + **kwargs: Additional arguments can be specified for different order + types. + + Returns: + dict: Order details. See `place_order` for example. + + """ + return self.place_order(product_id, 'sell', order_type, **kwargs) + + def place_limit_order(self, product_id, side, price, size, + client_oid=None, + stp=None, + time_in_force=None, + cancel_after=None, + post_only=None, + overdraft_enabled=None, + funding_amount=None): + """Place a limit order. + + Args: + product_id (str): Product to order (eg. 'BTC-USD') + side (str): Order side ('buy' or 'sell) + price (Decimal): Price per cryptocurrency + size (Decimal): Amount of cryptocurrency to buy or sell + client_oid (Optional[str]): User-specified Order ID + stp (Optional[str]): Self-trade prevention flag. See `place_order` + for details. + time_in_force (Optional[str]): Time in force. Options: + 'GTC' Good till canceled + 'GTT' Good till time (set by `cancel_after`) + 'IOC' Immediate or cancel + 'FOK' Fill or kill + cancel_after (Optional[str]): Cancel after this period for 'GTT' + orders. Options are 'min', 'hour', or 'day'. + post_only (Optional[bool]): Indicates that the order should only + make liquidity. If any part of the order results in taking + liquidity, the order will be rejected and no part of it will + execute. + overdraft_enabled (Optional[bool]): If true funding above and + beyond the account balance will be provided by margin, as + necessary. + funding_amount (Optional[Decimal]): Amount of margin funding to be + provided for the order. Mutually exclusive with + `overdraft_enabled`. + + Returns: + dict: Order details. See `place_order` for example. + + """ + params = {'product_id': product_id, + 'side': side, + 'order_type': 'limit', + 'price': price, + 'size': size, + 'client_oid': client_oid, + 'stp': stp, + 'time_in_force': time_in_force, + 'cancel_after': cancel_after, + 'post_only': post_only, + 'overdraft_enabled': overdraft_enabled, + 'funding_amount': funding_amount} + params = dict((k, v) for k, v in params.items() if v is not None) + + return self.place_order(**params) + + def place_market_order(self, product_id, side, size=None, funds=None, + client_oid=None, + stp=None, + overdraft_enabled=None, + funding_amount=None): + """ Place market order. + + Args: + product_id (str): Product to order (eg. 'BTC-USD') + side (str): Order side ('buy' or 'sell) + size (Optional[Decimal]): Desired amount in crypto. Specify this or + `funds`. + funds (Optional[Decimal]): Desired amount of quote currency to use. + Specify this or `size`. + client_oid (Optional[str]): User-specified Order ID + stp (Optional[str]): Self-trade prevention flag. See `place_order` + for details. + overdraft_enabled (Optional[bool]): If true funding above and + beyond the account balance will be provided by margin, as + necessary. + funding_amount (Optional[Decimal]): Amount of margin funding to be + provided for the order. Mutually exclusive with + `overdraft_enabled`. + + Returns: + dict: Order details. See `place_order` for example. + + """ + params = {'product_id': product_id, + 'side': side, + 'order_type': 'market', + 'size': size, + 'funds': funds, + 'client_oid': client_oid, + 'stp': stp, + 'overdraft_enabled': overdraft_enabled, + 'funding_amount': funding_amount} + params = dict((k, v) for k, v in params.items() if v is not None) + + return self.place_order(**params) + + def place_stop_order(self, product_id, side, price, size=None, funds=None, + client_oid=None, + stp=None, + overdraft_enabled=None, + funding_amount=None): + """ Place stop order. + + Args: + product_id (str): Product to order (eg. 'BTC-USD') + side (str): Order side ('buy' or 'sell) + price (Decimal): Desired price at which the stop order triggers. + size (Optional[Decimal]): Desired amount in crypto. Specify this or + `funds`. + funds (Optional[Decimal]): Desired amount of quote currency to use. + Specify this or `size`. + client_oid (Optional[str]): User-specified Order ID + stp (Optional[str]): Self-trade prevention flag. See `place_order` + for details. + overdraft_enabled (Optional[bool]): If true funding above and + beyond the account balance will be provided by margin, as + necessary. + funding_amount (Optional[Decimal]): Amount of margin funding to be + provided for the order. Mutually exclusive with + `overdraft_enabled`. + + Returns: + dict: Order details. See `place_order` for example. + + """ + params = {'product_id': product_id, + 'side': side, + 'price': price, + 'order_type': 'stop', + 'size': size, + 'funds': funds, + 'client_oid': client_oid, + 'stp': stp, + 'overdraft_enabled': overdraft_enabled, + 'funding_amount': funding_amount} + params = dict((k, v) for k, v in params.items() if v is not None) + + return self.place_order(**params) + + def cancel_order(self, order_id): + """ Cancel a previously placed order. + + If the order had no matches during its lifetime its record may + be purged. This means the order details will not be available + with get_order(order_id). If the order could not be canceled + (already filled or previously canceled, etc), then an error + response will indicate the reason in the message field. + + **Caution**: The order id is the server-assigned order id and + not the optional client_oid. + + Args: + order_id (str): The order_id of the order you want to cancel + + Returns: + list: Containing the order_id of cancelled order. Example:: + [ "c5ab5eae-76be-480e-8961-00792dc7e138" ] + + """ + return self._send_message('delete', '/orders/' + order_id) + + def cancel_all(self, product_id=None): + """ With best effort, cancel all open orders. + + Args: + product_id (Optional[str]): Only cancel orders for this + product_id + + Returns: + list: A list of ids of the canceled orders. Example:: + [ + "144c6f8e-713f-4682-8435-5280fbe8b2b4", + "debe4907-95dc-442f-af3b-cec12f42ebda", + "cf7aceee-7b08-4227-a76c-3858144323ab", + "dfc5ae27-cadb-4c0c-beef-8994936fde8a", + "34fecfbf-de33-4273-b2c6-baf8e8948be4" + ] + + """ + if product_id is not None: + params = {'product_id': product_id} + data = json.dumps(params) + else: + data = None + return self._send_message('delete', '/orders', data=data) + + def get_order(self, order_id): + """ Get a single order by order id. + + If the order is canceled the response may have status code 404 + if the order had no matches. + + **Caution**: Open orders may change state between the request + and the response depending on market conditions. + + Args: + order_id (str): The order to get information of. + + Returns: + dict: Containing information on order. Example:: + { + "created_at": "2017-06-18T00:27:42.920136Z", + "executed_value": "0.0000000000000000", + "fill_fees": "0.0000000000000000", + "filled_size": "0.00000000", + "id": "9456f388-67a9-4316-bad1-330c5353804f", + "post_only": true, + "price": "1.00000000", + "product_id": "BTC-USD", + "settled": false, + "side": "buy", + "size": "1.00000000", + "status": "pending", + "stp": "dc", + "time_in_force": "GTC", + "type": "limit" + } + + """ + return self._send_message('get', '/orders/' + order_id) + + def get_orders(self, product_id=None, status=None, **kwargs): + """ List your current open orders. + + This method returns a generator which may make multiple HTTP requests + while iterating through it. + + Only open or un-settled orders are returned. As soon as an + order is no longer open and settled, it will no longer appear + in the default request. + + Orders which are no longer resting on the order book, will be + marked with the 'done' status. There is a small window between + an order being 'done' and 'settled'. An order is 'settled' when + all of the fills have settled and the remaining holds (if any) + have been removed. + + For high-volume trading it is strongly recommended that you + maintain your own list of open orders and use one of the + streaming market data feeds to keep it updated. You should poll + the open orders endpoint once when you start trading to obtain + the current state of any open orders. + + Args: + product_id (Optional[str]): Only list orders for this + product + status (Optional[list/str]): Limit list of orders to + this status or statuses. Passing 'all' returns orders + of all statuses. + ** Options: 'open', 'pending', 'active', 'done', + 'settled' + ** default: ['open', 'pending', 'active'] + + Returns: + list: Containing information on orders. Example:: + [ + { + "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2", + "price": "0.10000000", + "size": "0.01000000", + "product_id": "BTC-USD", + "side": "buy", + "stp": "dc", + "type": "limit", + "time_in_force": "GTC", + "post_only": false, + "created_at": "2016-12-08T20:02:28.53864Z", + "fill_fees": "0.0000000000000000", + "filled_size": "0.00000000", + "executed_value": "0.0000000000000000", + "status": "open", + "settled": false + }, + { + ... + } + ] + + """ + params = kwargs + if product_id is not None: + params['product_id'] = product_id + if status is not None: + params['status'] = status + return self._send_paginated_message('/orders', params=params) + + def get_fills(self, product_id=None, order_id=None, **kwargs): + """ Get a list of recent fills. + + This method returns a generator which may make multiple HTTP requests + while iterating through it. + + Fees are recorded in two stages. Immediately after the matching + engine completes a match, the fill is inserted into our + datastore. Once the fill is recorded, a settlement process will + settle the fill and credit both trading counterparties. + + The 'fee' field indicates the fees charged for this fill. + + The 'liquidity' field indicates if the fill was the result of a + liquidity provider or liquidity taker. M indicates Maker and T + indicates Taker. + + As of 8/23/18 - Requests without either order_id or product_id + will be rejected + + Args: + product_id (str): Limit list to this product_id + order_id (str): Limit list to this order_id + kwargs (dict): Additional HTTP request parameters. + + Returns: + list: Containing information on fills. Example:: + [ + { + "trade_id": 74, + "product_id": "BTC-USD", + "price": "10.00", + "size": "0.01", + "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", + "created_at": "2014-11-07T22:19:28.578544Z", + "liquidity": "T", + "fee": "0.00025", + "settled": true, + "side": "buy" + }, + { + ... + } + ] + + """ + params = {} + if product_id: + params['product_id'] = product_id + if order_id: + params['order_id'] = order_id + params.update(kwargs) + + return self._send_paginated_message('/fills', params=params) + + def get_fundings(self, status=None, **kwargs): + """ Every order placed with a margin profile that draws funding + will create a funding record. + + This method returns a generator which may make multiple HTTP requests + while iterating through it. + + Args: + status (list/str): Limit funding records to these statuses. + ** Options: 'outstanding', 'settled', 'rejected' + kwargs (dict): Additional HTTP request parameters. + + Returns: + list: Containing information on margin funding. Example:: + [ + { + "id": "b93d26cd-7193-4c8d-bfcc-446b2fe18f71", + "order_id": "b93d26cd-7193-4c8d-bfcc-446b2fe18f71", + "profile_id": "d881e5a6-58eb-47cd-b8e2-8d9f2e3ec6f6", + "amount": "1057.6519956381537500", + "status": "settled", + "created_at": "2017-03-17T23:46:16.663397Z", + "currency": "USD", + "repaid_amount": "1057.6519956381537500", + "default_amount": "0", + "repaid_default": false + }, + { + ... + } + ] + + """ + params = {} + if status is not None: + params['status'] = status + params.update(kwargs) + return self._send_paginated_message('/funding', params=params) + + def repay_funding(self, amount, currency): + """ Repay funding. Repays the older funding records first. + + Args: + amount (int): Amount of currency to repay + currency (str): The currency, example USD + + Returns: + Not specified by cbpro. + + """ + params = { + 'amount': amount, + 'currency': currency # example: USD + } + return self._send_message('post', '/funding/repay', + data=json.dumps(params)) + + def margin_transfer(self, margin_profile_id, transfer_type, currency, + amount): + """ Transfer funds between your standard profile and a margin profile. + + Args: + margin_profile_id (str): Margin profile ID to withdraw or deposit + from. + transfer_type (str): 'deposit' or 'withdraw' + currency (str): Currency to transfer (eg. 'USD') + amount (Decimal): Amount to transfer + + Returns: + dict: Transfer details. Example:: + { + "created_at": "2017-01-25T19:06:23.415126Z", + "id": "80bc6b74-8b1f-4c60-a089-c61f9810d4ab", + "user_id": "521c20b3d4ab09621f000011", + "profile_id": "cda95996-ac59-45a3-a42e-30daeb061867", + "margin_profile_id": "45fa9e3b-00ba-4631-b907-8a98cbdf21be", + "type": "deposit", + "amount": "2", + "currency": "USD", + "account_id": "23035fc7-0707-4b59-b0d2-95d0c035f8f5", + "margin_account_id": "e1d9862c-a259-4e83-96cd-376352a9d24d", + "margin_product_id": "BTC-USD", + "status": "completed", + "nonce": 25 + } + + """ + params = {'margin_profile_id': margin_profile_id, + 'type': transfer_type, + 'currency': currency, # example: USD + 'amount': amount} + return self._send_message('post', '/profiles/margin-transfer', + data=json.dumps(params)) + + def get_position(self): + """ Get An overview of your margin profile. + + Returns: + dict: Details about funding, accounts, and margin call. + + """ + return self._send_message('get', '/position') + + def close_position(self, repay_only): + """ Close position. + + Args: + repay_only (bool): Undocumented by cbpro. + + Returns: + Undocumented + + """ + params = {'repay_only': repay_only} + return self._send_message('post', '/position/close', + data=json.dumps(params)) + + def deposit(self, amount, currency, payment_method_id): + """ Deposit funds from a payment method. + + See AuthenticatedClient.get_payment_methods() to receive + information regarding payment methods. + + Args: + amount (Decmial): The amount to deposit. + currency (str): The type of currency. + payment_method_id (str): ID of the payment method. + + Returns: + dict: Information about the deposit. Example:: + { + "id": "593533d2-ff31-46e0-b22e-ca754147a96a", + "amount": "10.00", + "currency": "USD", + "payout_at": "2016-08-20T00:31:09Z" + } + + """ + params = {'amount': amount, + 'currency': currency, + 'payment_method_id': payment_method_id} + return self._send_message('post', '/deposits/payment-method', + data=json.dumps(params)) + + def coinbase_deposit(self, amount, currency, coinbase_account_id): + """ Deposit funds from a coinbase account. + + You can move funds between your Coinbase accounts and your cbpro + trading accounts within your daily limits. Moving funds between + Coinbase and cbpro is instant and free. + + See AuthenticatedClient.get_coinbase_accounts() to receive + information regarding your coinbase_accounts. + + Args: + amount (Decimal): The amount to deposit. + currency (str): The type of currency. + coinbase_account_id (str): ID of the coinbase account. + + Returns: + dict: Information about the deposit. Example:: + { + "id": "593533d2-ff31-46e0-b22e-ca754147a96a", + "amount": "10.00", + "currency": "BTC", + } + + """ + params = {'amount': amount, + 'currency': currency, + 'coinbase_account_id': coinbase_account_id} + return self._send_message('post', '/deposits/coinbase-account', + data=json.dumps(params)) + + def withdraw(self, amount, currency, payment_method_id): + """ Withdraw funds to a payment method. + + See AuthenticatedClient.get_payment_methods() to receive + information regarding payment methods. + + Args: + amount (Decimal): The amount to withdraw. + currency (str): Currency type (eg. 'BTC') + payment_method_id (str): ID of the payment method. + + Returns: + dict: Withdraw details. Example:: + { + "id":"593533d2-ff31-46e0-b22e-ca754147a96a", + "amount": "10.00", + "currency": "USD", + "payout_at": "2016-08-20T00:31:09Z" + } + + """ + params = {'amount': amount, + 'currency': currency, + 'payment_method_id': payment_method_id} + return self._send_message('post', '/withdrawals/payment-method', + data=json.dumps(params)) + + def coinbase_withdraw(self, amount, currency, coinbase_account_id): + """ Withdraw funds to a coinbase account. + + You can move funds between your Coinbase accounts and your cbpro + trading accounts within your daily limits. Moving funds between + Coinbase and cbpro is instant and free. + + See AuthenticatedClient.get_coinbase_accounts() to receive + information regarding your coinbase_accounts. + + Args: + amount (Decimal): The amount to withdraw. + currency (str): The type of currency (eg. 'BTC') + coinbase_account_id (str): ID of the coinbase account. + + Returns: + dict: Information about the deposit. Example:: + { + "id":"593533d2-ff31-46e0-b22e-ca754147a96a", + "amount":"10.00", + "currency": "BTC", + } + + """ + params = {'amount': amount, + 'currency': currency, + 'coinbase_account_id': coinbase_account_id} + return self._send_message('post', '/withdrawals/coinbase', + data=json.dumps(params)) + + def crypto_withdraw(self, amount, currency, crypto_address): + """ Withdraw funds to a crypto address. + + Args: + amount (Decimal): The amount to withdraw + currency (str): The type of currency (eg. 'BTC') + crypto_address (str): Crypto address to withdraw to. + + Returns: + dict: Withdraw details. Example:: + { + "id":"593533d2-ff31-46e0-b22e-ca754147a96a", + "amount":"10.00", + "currency": "BTC", + } + + """ + params = {'amount': amount, + 'currency': currency, + 'crypto_address': crypto_address} + return self._send_message('post', '/withdrawals/crypto', + data=json.dumps(params)) + + def get_payment_methods(self): + """ Get a list of your payment methods. + + Returns: + list: Payment method details. + + """ + return self._send_message('get', '/payment-methods') + + def get_coinbase_accounts(self): + """ Get a list of your coinbase accounts. + + Returns: + list: Coinbase account details. + + """ + return self._send_message('get', '/coinbase-accounts') + + def create_report(self, report_type, start_date, end_date, product_id=None, + account_id=None, report_format='pdf', email=None): + """ Create report of historic information about your account. + + The report will be generated when resources are available. Report status + can be queried via `get_report(report_id)`. + + Args: + report_type (str): 'fills' or 'account' + start_date (str): Starting date for the report in ISO 8601 + end_date (str): Ending date for the report in ISO 8601 + product_id (Optional[str]): ID of the product to generate a fills + report for. Required if account_type is 'fills' + account_id (Optional[str]): ID of the account to generate an account + report for. Required if report_type is 'account'. + report_format (Optional[str]): 'pdf' or 'csv'. Default is 'pdf'. + email (Optional[str]): Email address to send the report to. + + Returns: + dict: Report details. Example:: + { + "id": "0428b97b-bec1-429e-a94c-59232926778d", + "type": "fills", + "status": "pending", + "created_at": "2015-01-06T10:34:47.000Z", + "completed_at": undefined, + "expires_at": "2015-01-13T10:35:47.000Z", + "file_url": undefined, + "params": { + "start_date": "2014-11-01T00:00:00.000Z", + "end_date": "2014-11-30T23:59:59.000Z" + } + } + + """ + params = {'type': report_type, + 'start_date': start_date, + 'end_date': end_date, + 'format': report_format} + if product_id is not None: + params['product_id'] = product_id + if account_id is not None: + params['account_id'] = account_id + if email is not None: + params['email'] = email + + return self._send_message('post', '/reports', + data=json.dumps(params)) + + def get_report(self, report_id): + """ Get report status. + + Use to query a specific report once it has been requested. + + Args: + report_id (str): Report ID + + Returns: + dict: Report details, including file url once it is created. + + """ + return self._send_message('get', '/reports/' + report_id) + + def get_trailing_volume(self): + """ Get your 30-day trailing volume for all products. + + This is a cached value that's calculated every day at midnight UTC. + + Returns: + list: 30-day trailing volumes. Example:: + [ + { + "product_id": "BTC-USD", + "exchange_volume": "11800.00000000", + "volume": "100.00000000", + "recorded_at": "1973-11-29T00:05:01.123456Z" + }, + { + ... + } + ] + + """ + return self._send_message('get', '/users/self/trailing-volume') diff --git a/cbpro/cbpro_auth.py b/cbpro/cbpro_auth.py new file mode 100644 index 00000000..db6561fe --- /dev/null +++ b/cbpro/cbpro_auth.py @@ -0,0 +1,37 @@ +import hmac +import hashlib +import time +import base64 +from requests.auth import AuthBase + + +class CBProAuth(AuthBase): + # Provided by CBPro: https://docs.pro.coinbase.com/#signing-a-message + def __init__(self, api_key, secret_key, passphrase): + self.api_key = api_key + self.secret_key = secret_key + self.passphrase = passphrase + + def __call__(self, request): + timestamp = str(time.time()) + message = ''.join([timestamp, request.method, + request.path_url, (request.body or '')]) + request.headers.update(get_auth_headers(timestamp, message, + self.api_key, + self.secret_key, + self.passphrase)) + return request + + +def get_auth_headers(timestamp, message, api_key, secret_key, passphrase): + message = message.encode('ascii') + hmac_key = base64.b64decode(secret_key) + signature = hmac.new(hmac_key, message, hashlib.sha256) + signature_b64 = base64.b64encode(signature.digest()).decode('utf-8') + return { + 'Content-Type': 'Application/JSON', + 'CB-ACCESS-SIGN': signature_b64, + 'CB-ACCESS-TIMESTAMP': timestamp, + 'CB-ACCESS-KEY': api_key, + 'CB-ACCESS-PASSPHRASE': passphrase + } diff --git a/cbpro/order_book.py b/cbpro/order_book.py new file mode 100644 index 00000000..d3ea56eb --- /dev/null +++ b/cbpro/order_book.py @@ -0,0 +1,298 @@ +# +# gdax/order_book.py +# David Caseria +# +# Live order book updated from the gdax Websocket Feed + +from sortedcontainers import SortedDict +from decimal import Decimal +import pickle + +from gdax.public_client import PublicClient +from gdax.websocket_client import WebsocketClient + + +class OrderBook(WebsocketClient): + def __init__(self, product_id='BTC-USD', log_to=None): + super(OrderBook, self).__init__(products=product_id) + self._asks = SortedDict() + self._bids = SortedDict() + self._client = PublicClient() + self._sequence = -1 + self._log_to = log_to + if self._log_to: + assert hasattr(self._log_to, 'write') + self._current_ticker = None + + @property + def product_id(self): + ''' Currently OrderBook only supports a single product even though it is stored as a list of products. ''' + return self.products[0] + + def on_open(self): + self._sequence = -1 + print("-- Subscribed to OrderBook! --\n") + + def on_close(self): + print("\n-- OrderBook Socket Closed! --") + + def reset_book(self): + self._asks = SortedDict() + self._bids = SortedDict() + res = self._client.get_product_order_book(product_id=self.product_id, level=3) + for bid in res['bids']: + self.add({ + 'id': bid[2], + 'side': 'buy', + 'price': Decimal(bid[0]), + 'size': Decimal(bid[1]) + }) + for ask in res['asks']: + self.add({ + 'id': ask[2], + 'side': 'sell', + 'price': Decimal(ask[0]), + 'size': Decimal(ask[1]) + }) + self._sequence = res['sequence'] + + def on_message(self, message): + if self._log_to: + pickle.dump(message, self._log_to) + + sequence = message.get('sequence', -1) + if self._sequence == -1: + self.reset_book() + return + if sequence <= self._sequence: + # ignore older messages (e.g. before order book initialization from getProductOrderBook) + return + elif sequence > self._sequence + 1: + self.on_sequence_gap(self._sequence, sequence) + return + + msg_type = message['type'] + if msg_type == 'open': + self.add(message) + elif msg_type == 'done' and 'price' in message: + self.remove(message) + elif msg_type == 'match': + self.match(message) + self._current_ticker = message + elif msg_type == 'change': + self.change(message) + + self._sequence = sequence + + def on_sequence_gap(self, gap_start, gap_end): + self.reset_book() + print('Error: messages missing ({} - {}). Re-initializing book at sequence.'.format( + gap_start, gap_end, self._sequence)) + + + def add(self, order): + order = { + 'id': order.get('order_id') or order['id'], + 'side': order['side'], + 'price': Decimal(order['price']), + 'size': Decimal(order.get('size') or order['remaining_size']) + } + if order['side'] == 'buy': + bids = self.get_bids(order['price']) + if bids is None: + bids = [order] + else: + bids.append(order) + self.set_bids(order['price'], bids) + else: + asks = self.get_asks(order['price']) + if asks is None: + asks = [order] + else: + asks.append(order) + self.set_asks(order['price'], asks) + + def remove(self, order): + price = Decimal(order['price']) + if order['side'] == 'buy': + bids = self.get_bids(price) + if bids is not None: + bids = [o for o in bids if o['id'] != order['order_id']] + if len(bids) > 0: + self.set_bids(price, bids) + else: + self.remove_bids(price) + else: + asks = self.get_asks(price) + if asks is not None: + asks = [o for o in asks if o['id'] != order['order_id']] + if len(asks) > 0: + self.set_asks(price, asks) + else: + self.remove_asks(price) + + def match(self, order): + size = Decimal(order['size']) + price = Decimal(order['price']) + + if order['side'] == 'buy': + bids = self.get_bids(price) + if not bids: + return + assert bids[0]['id'] == order['maker_order_id'] + if bids[0]['size'] == size: + self.set_bids(price, bids[1:]) + else: + bids[0]['size'] -= size + self.set_bids(price, bids) + else: + asks = self.get_asks(price) + if not asks: + return + assert asks[0]['id'] == order['maker_order_id'] + if asks[0]['size'] == size: + self.set_asks(price, asks[1:]) + else: + asks[0]['size'] -= size + self.set_asks(price, asks) + + def change(self, order): + try: + new_size = Decimal(order['new_size']) + except KeyError: + return + + try: + price = Decimal(order['price']) + except KeyError: + return + + if order['side'] == 'buy': + bids = self.get_bids(price) + if bids is None or not any(o['id'] == order['order_id'] for o in bids): + return + index = [b['id'] for b in bids].index(order['order_id']) + bids[index]['size'] = new_size + self.set_bids(price, bids) + else: + asks = self.get_asks(price) + if asks is None or not any(o['id'] == order['order_id'] for o in asks): + return + index = [a['id'] for a in asks].index(order['order_id']) + asks[index]['size'] = new_size + self.set_asks(price, asks) + + tree = self._asks if order['side'] == 'sell' else self._bids + node = tree.get(price) + + if node is None or not any(o['id'] == order['order_id'] for o in node): + return + + def get_current_ticker(self): + return self._current_ticker + + def get_current_book(self): + result = { + 'sequence': self._sequence, + 'asks': [], + 'bids': [], + } + for ask in self._asks: + try: + # There can be a race condition here, where a price point is removed + # between these two ops + this_ask = self._asks[ask] + except KeyError: + continue + for order in this_ask: + result['asks'].append([order['price'], order['size'], order['id']]) + for bid in self._bids: + try: + # There can be a race condition here, where a price point is removed + # between these two ops + this_bid = self._bids[bid] + except KeyError: + continue + + for order in this_bid: + result['bids'].append([order['price'], order['size'], order['id']]) + return result + + def get_ask(self): + return self._asks.peekitem(0)[0] + + def get_asks(self, price): + return self._asks.get(price) + + def remove_asks(self, price): + del self._asks[price] + + def set_asks(self, price, asks): + self._asks[price] = asks + + def get_bid(self): + return self._bids.peekitem(-1)[0] + + def get_bids(self, price): + return self._bids.get(price) + + def remove_bids(self, price): + del self._bids[price] + + def set_bids(self, price, bids): + self._bids[price] = bids + + +if __name__ == '__main__': + import sys + import time + import datetime as dt + + + class OrderBookConsole(OrderBook): + ''' Logs real-time changes to the bid-ask spread to the console ''' + + def __init__(self, product_id=None): + super(OrderBookConsole, self).__init__(product_id=product_id) + + # latest values of bid-ask spread + self._bid = None + self._ask = None + self._bid_depth = None + self._ask_depth = None + + def on_message(self, message): + super(OrderBookConsole, self).on_message(message) + + # Calculate newest bid-ask spread + bid = self.get_bid() + bids = self.get_bids(bid) + bid_depth = sum([b['size'] for b in bids]) + ask = self.get_ask() + asks = self.get_asks(ask) + ask_depth = sum([a['size'] for a in asks]) + + if self._bid == bid and self._ask == ask and self._bid_depth == bid_depth and self._ask_depth == ask_depth: + # If there are no changes to the bid-ask spread since the last update, no need to print + pass + else: + # If there are differences, update the cache + self._bid = bid + self._ask = ask + self._bid_depth = bid_depth + self._ask_depth = ask_depth + print('{} {} bid: {:.3f} @ {:.2f}\task: {:.3f} @ {:.2f}'.format( + dt.datetime.now(), self.product_id, bid_depth, bid, ask_depth, ask)) + + order_book = OrderBookConsole() + order_book.start() + try: + while True: + time.sleep(10) + except KeyboardInterrupt: + order_book.close() + + if order_book.error: + sys.exit(1) + else: + sys.exit(0) diff --git a/cbpro/public_client.py b/cbpro/public_client.py new file mode 100644 index 00000000..52528964 --- /dev/null +++ b/cbpro/public_client.py @@ -0,0 +1,310 @@ +# +# cbpro/PublicClient.py +# Daniel Paquin +# +# For public requests to the Coinbase exchange + +import requests + + +class PublicClient(object): + """cbpro public client API. + + All requests default to the `product_id` specified at object + creation if not otherwise specified. + + Attributes: + url (Optional[str]): API URL. Defaults to cbpro API. + + """ + + def __init__(self, api_url='https://api.pro.coinbase.com', timeout=30): + """Create cbpro API public client. + + Args: + api_url (Optional[str]): API URL. Defaults to cbpro API. + + """ + self.url = api_url.rstrip('/') + self.auth = None + self.session = requests.Session() + + def get_products(self): + """Get a list of available currency pairs for trading. + + Returns: + list: Info about all currency pairs. Example:: + [ + { + "id": "BTC-USD", + "display_name": "BTC/USD", + "base_currency": "BTC", + "quote_currency": "USD", + "base_min_size": "0.01", + "base_max_size": "10000.00", + "quote_increment": "0.01" + } + ] + + """ + return self._send_message('get', '/products') + + def get_product_order_book(self, product_id, level=1): + """Get a list of open orders for a product. + + The amount of detail shown can be customized with the `level` + parameter: + * 1: Only the best bid and ask + * 2: Top 50 bids and asks (aggregated) + * 3: Full order book (non aggregated) + + Level 1 and Level 2 are recommended for polling. For the most + up-to-date data, consider using the websocket stream. + + **Caution**: Level 3 is only recommended for users wishing to + maintain a full real-time order book using the websocket + stream. Abuse of Level 3 via polling will cause your access to + be limited or blocked. + + Args: + product_id (str): Product + level (Optional[int]): Order book level (1, 2, or 3). + Default is 1. + + Returns: + dict: Order book. Example for level 1:: + { + "sequence": "3", + "bids": [ + [ price, size, num-orders ], + ], + "asks": [ + [ price, size, num-orders ], + ] + } + + """ + params = {'level': level} + return self._send_message('get', + '/products/{}/book'.format(product_id), + params=params) + + def get_product_ticker(self, product_id): + """Snapshot about the last trade (tick), best bid/ask and 24h volume. + + **Caution**: Polling is discouraged in favor of connecting via + the websocket stream and listening for match messages. + + Args: + product_id (str): Product + + Returns: + dict: Ticker info. Example:: + { + "trade_id": 4729088, + "price": "333.99", + "size": "0.193", + "bid": "333.98", + "ask": "333.99", + "volume": "5957.11914015", + "time": "2015-11-14T20:46:03.511254Z" + } + + """ + return self._send_message('get', + '/products/{}/ticker'.format(product_id)) + + def get_product_trades(self, product_id, before='', after='', limit=None, result=None): + """List the latest trades for a product. + + This method returns a generator which may make multiple HTTP requests + while iterating through it. + + Args: + product_id (str): Product + before (Optional[str]): start time in ISO 8601 + after (Optional[str]): end time in ISO 8601 + limit (Optional[int]): the desired number of trades (can be more than 100, + automatically paginated) + results (Optional[list]): list of results that is used for the pagination + Returns: + list: Latest trades. Example:: + [{ + "time": "2014-11-07T22:19:28.578544Z", + "trade_id": 74, + "price": "10.00000000", + "size": "0.01000000", + "side": "buy" + }, { + "time": "2014-11-07T01:08:43.642366Z", + "trade_id": 73, + "price": "100.00000000", + "size": "0.01000000", + "side": "sell" + }] + """ + return self._send_paginated_message('/products/{}/trades' + .format(product_id)) + + def get_product_historic_rates(self, product_id, start=None, end=None, + granularity=None): + """Historic rates for a product. + + Rates are returned in grouped buckets based on requested + `granularity`. If start, end, and granularity aren't provided, + the exchange will assume some (currently unknown) default values. + + Historical rate data may be incomplete. No data is published for + intervals where there are no ticks. + + **Caution**: Historical rates should not be polled frequently. + If you need real-time information, use the trade and book + endpoints along with the websocket feed. + + The maximum number of data points for a single request is 200 + candles. If your selection of start/end time and granularity + will result in more than 200 data points, your request will be + rejected. If you wish to retrieve fine granularity data over a + larger time range, you will need to make multiple requests with + new start/end ranges. + + Args: + product_id (str): Product + start (Optional[str]): Start time in ISO 8601 + end (Optional[str]): End time in ISO 8601 + granularity (Optional[int]): Desired time slice in seconds + + Returns: + list: Historic candle data. Example: + [ + [ time, low, high, open, close, volume ], + [ 1415398768, 0.32, 4.2, 0.35, 4.2, 12.3 ], + ... + ] + + """ + params = {} + if start is not None: + params['start'] = start + if end is not None: + params['end'] = end + if granularity is not None: + acceptedGrans = [60, 300, 900, 3600, 21600, 86400] + if granularity not in acceptedGrans: + raise ValueError( 'Specified granularity is {}, must be in approved values: {}'.format( + granularity, acceptedGrans) ) + + params['granularity'] = granularity + return self._send_message('get', + '/products/{}/candles'.format(product_id)) + + def get_product_24hr_stats(self, product_id): + """Get 24 hr stats for the product. + + Args: + product_id (str): Product + + Returns: + dict: 24 hour stats. Volume is in base currency units. + Open, high, low are in quote currency units. Example:: + { + "open": "34.19000000", + "high": "95.70000000", + "low": "7.06000000", + "volume": "2.41000000" + } + + """ + return self._send_message('get', + '/products/{}/stats'.format(product_id)) + + def get_currencies(self): + """List known currencies. + + Returns: + list: List of currencies. Example:: + [{ + "id": "BTC", + "name": "Bitcoin", + "min_size": "0.00000001" + }, { + "id": "USD", + "name": "United States Dollar", + "min_size": "0.01000000" + }] + + """ + return self._send_message('get', '/currencies') + + def get_time(self): + """Get the API server time. + + Returns: + dict: Server time in ISO and epoch format (decimal seconds + since Unix epoch). Example:: + { + "iso": "2015-01-07T23:47:25.201Z", + "epoch": 1420674445.201 + } + + """ + return self._send_message('get', '/time') + + def _send_message(self, method, endpoint, params=None, data=None): + """Send API request. + + Args: + method (str): HTTP method (get, post, delete, etc.) + endpoint (str): Endpoint (to be added to base URL) + params (Optional[dict]): HTTP request parameters + data (Optional[str]): JSON-encoded string payload for POST + + Returns: + dict/list: JSON response + + """ + url = self.url + endpoint + r = self.session.request(method, url, params=params, data=data, + auth=self.auth, timeout=30) + return r.json() + + def _send_paginated_message(self, endpoint, params=None): + """ Send API message that results in a paginated response. + + The paginated responses are abstracted away by making API requests on + demand as the response is iterated over. + + Paginated API messages support 3 additional parameters: `before`, + `after`, and `limit`. `before` and `after` are mutually exclusive. To + use them, supply an index value for that endpoint (the field used for + indexing varies by endpoint - get_fills() uses 'trade_id', for example). + `before`: Only get data that occurs more recently than index + `after`: Only get data that occurs further in the past than index + `limit`: Set amount of data per HTTP response. Default (and + maximum) of 100. + + Args: + endpoint (str): Endpoint (to be added to base URL) + params (Optional[dict]): HTTP request parameters + + Yields: + dict: API response objects + + """ + if params is None: + params = dict() + url = self.url + endpoint + while True: + r = self.session.get(url, params=params, auth=self.auth, timeout=30) + results = r.json() + for result in results: + yield result + # If there are no more pages, we're done. Otherwise update `after` + # param to get next page. + # If this request included `before` don't get any more pages - the + # cbpro API doesn't support multiple pages in that case. + if not r.headers.get('cb-after') or \ + params.get('before') is not None: + break + else: + params['after'] = r.headers['cb-after'] diff --git a/cbpro/websocket_client.py b/cbpro/websocket_client.py new file mode 100644 index 00000000..861b7444 --- /dev/null +++ b/cbpro/websocket_client.py @@ -0,0 +1,160 @@ +# cbpro/WebsocketClient.py +# original author: Daniel Paquin +# mongo "support" added by Drew Rice +# +# +# Template object to receive messages from the Coinbase Websocket Feed + +from __future__ import print_function +import json +import base64 +import hmac +import hashlib +import time +from threading import Thread +from websocket import create_connection, WebSocketConnectionClosedException +from pymongo import MongoClient +from cbpro.cbpro_auth import get_auth_headers + + +class WebsocketClient(object): + def __init__(self, url="wss://ws-feed.pro.coinbase.com", products=None, message_type="subscribe", mongo_collection=None, + should_print=True, auth=False, api_key="", api_secret="", api_passphrase="", channels=None): + self.url = url + self.products = products + self.channels = channels + self.type = message_type + self.stop = False + self.error = None + self.ws = None + self.thread = None + self.auth = auth + self.api_key = api_key + self.api_secret = api_secret + self.api_passphrase = api_passphrase + self.should_print = should_print + self.mongo_collection = mongo_collection + + def start(self): + def _go(): + self._connect() + self._listen() + self._disconnect() + + self.stop = False + self.on_open() + self.thread = Thread(target=_go) + self.thread.start() + + def _connect(self): + if self.products is None: + self.products = ["BTC-USD"] + elif not isinstance(self.products, list): + self.products = [self.products] + + if self.url[-1] == "/": + self.url = self.url[:-1] + + if self.channels is None: + sub_params = {'type': 'subscribe', 'product_ids': self.products} + else: + sub_params = {'type': 'subscribe', 'product_ids': self.products, 'channels': self.channels} + + if self.auth: + timestamp = str(time.time()) + message = timestamp + 'GET' + '/users/self/verify' + auth_headers = get_auth_headers(timestamp, message, self.api_key, self.api_secret, self.api_passphrase) + sub_params['signature'] = auth_headers['CB-ACCESS-SIGN'] + sub_params['key'] = auth_headers['CB-ACCESS-SIGN'] + sub_params['passphrase'] = auth_headers['CB-ACCESS-KEY'] + sub_params['timestamp'] = auth_headers['CB-ACCESS-TIMESTAMP'] + + self.ws = create_connection(self.url) + + self.ws.send(json.dumps(sub_params)) + + def _listen(self): + while not self.stop: + try: + start_t = 0 + if time.time() - start_t >= 30: + # Set a 30 second ping to keep connection alive + self.ws.ping("keepalive") + start_t = time.time() + data = self.ws.recv() + msg = json.loads(data) + except ValueError as e: + self.on_error(e) + except Exception as e: + self.on_error(e) + else: + self.on_message(msg) + + def _disconnect(self): + try: + if self.ws: + self.ws.close() + except WebSocketConnectionClosedException as e: + pass + + self.on_close() + + def close(self): + self.stop = True + self.thread.join() + + def on_open(self): + if self.should_print: + print("-- Subscribed! --\n") + + def on_close(self): + if self.should_print: + print("\n-- Socket Closed --") + + def on_message(self, msg): + if self.should_print: + print(msg) + if self.mongo_collection: # dump JSON to given mongo collection + self.mongo_collection.insert_one(msg) + + def on_error(self, e, data=None): + self.error = e + self.stop = True + print('{} - data: {}'.format(e, data)) + + +if __name__ == "__main__": + import sys + import cbpro + import time + + + class MyWebsocketClient(cbpro.WebsocketClient): + def on_open(self): + self.url = "wss://ws-feed.pro.coinbase.com/" + self.products = ["BTC-USD", "ETH-USD"] + self.message_count = 0 + print("Let's count the messages!") + + def on_message(self, msg): + print(json.dumps(msg, indent=4, sort_keys=True)) + self.message_count += 1 + + def on_close(self): + print("-- Goodbye! --") + + + wsClient = MyWebsocketClient() + wsClient.start() + print(wsClient.url, wsClient.products) + try: + while True: + print("\nMessageCount =", "%i \n" % wsClient.message_count) + time.sleep(1) + except KeyboardInterrupt: + wsClient.close() + + if wsClient.error: + sys.exit(1) + else: + sys.exit(0) diff --git a/pytest.ini b/pytest.ini index ab35919d..094d739e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,3 @@ [pytest] -addopts = --cov gdax/ --cov-report=term-missing +addopts = --cov cbpro/ --cov-report=term-missing testpaths = tests \ No newline at end of file diff --git a/setup.py b/setup.py index 15a83d03..e68cbcc6 100644 --- a/setup.py +++ b/setup.py @@ -14,19 +14,25 @@ 'pytest', ] +with open("README.md", "r") as fh: + long_description = fh.read() + setup( - name='gdax', - version='1.0.6', + name='cbpro', + version='1.1.0', author='Daniel Paquin', author_email='dpaq34@gmail.com', license='MIT', - url='https://github.com/danpaquin/gdax-python', + url='https://github.com/danpaquin/coinbasepro-python', packages=find_packages(), install_requires=install_requires, tests_require=tests_require, - description='The unofficial Python client for the GDAX API', - download_url='https://github.com/danpaquin/gdax-Python/archive/master.zip', - keywords=['gdax', 'gdax-api', 'orderbook', 'trade', 'bitcoin', 'ethereum', 'BTC', 'ETH', 'client', 'api', 'wrapper', 'exchange', 'crypto', 'currency', 'trading', 'trading-api', 'coinbase'], + description='The unofficial Python client for the Coinbase Pro API', + long_description=long_description, + long_description_content_type="text/markdown", + download_url='https://github.com/danpaquin/coinbasepro-python/archive/master.zip', + keywords=['gdax', 'gdax-api', 'orderbook', 'trade', 'bitcoin', 'ethereum', 'BTC', 'ETH', 'client', 'api', 'wrapper', + 'exchange', 'crypto', 'currency', 'trading', 'trading-api', 'coinbase', 'pro', 'prime', 'coinbasepro'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', From a35362082932aa4dbaef80e61ccc5b89836c43bb Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Sun, 19 Aug 2018 19:11:13 -0400 Subject: [PATCH 125/174] remove gdax/ --- gdax/.gitignore | 2 - gdax/__init__.py | 4 - gdax/authenticated_client.py | 1017 ---------------------------------- gdax/gdax_auth.py | 37 -- gdax/order_book.py | 298 ---------- gdax/public_client.py | 310 ----------- gdax/websocket_client.py | 163 ------ 7 files changed, 1831 deletions(-) delete mode 100644 gdax/.gitignore delete mode 100644 gdax/__init__.py delete mode 100644 gdax/authenticated_client.py delete mode 100644 gdax/gdax_auth.py delete mode 100644 gdax/order_book.py delete mode 100644 gdax/public_client.py delete mode 100644 gdax/websocket_client.py diff --git a/gdax/.gitignore b/gdax/.gitignore deleted file mode 100644 index 79c40e7a..00000000 --- a/gdax/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.pyc -FixClient.py \ No newline at end of file diff --git a/gdax/__init__.py b/gdax/__init__.py deleted file mode 100644 index cb5fda32..00000000 --- a/gdax/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from gdax.authenticated_client import AuthenticatedClient -from gdax.public_client import PublicClient -from gdax.websocket_client import WebsocketClient -from gdax.order_book import OrderBook diff --git a/gdax/authenticated_client.py b/gdax/authenticated_client.py deleted file mode 100644 index 75509b4f..00000000 --- a/gdax/authenticated_client.py +++ /dev/null @@ -1,1017 +0,0 @@ -# -# gdax/AuthenticatedClient.py -# Daniel Paquin -# -# For authenticated requests to the gdax exchange - -import hmac -import hashlib -import time -import requests -import base64 -import json -from requests.auth import AuthBase -from gdax.public_client import PublicClient -from gdax.gdax_auth import GdaxAuth - - -class AuthenticatedClient(PublicClient): - """ Provides access to Private Endpoints on the GDAX API. - - All requests default to the live `api_url`: 'https://api.gdax.com'. - To test your application using the sandbox modify the `api_url`. - - Attributes: - url (str): The api url for this client instance to use. - auth (GdaxAuth): Custom authentication handler for each request. - session (requests.Session): Persistent HTTP connection object. - """ - def __init__(self, key, b64secret, passphrase, - api_url="https://api.gdax.com"): - """ Create an instance of the AuthenticatedClient class. - - Args: - key (str): Your API key. - b64secret (str): The secret key matching your API key. - passphrase (str): Passphrase chosen when setting up key. - api_url (Optional[str]): API URL. Defaults to GDAX API. - """ - super(AuthenticatedClient, self).__init__(api_url) - self.auth = GdaxAuth(key, b64secret, passphrase) - self.session = requests.Session() - - def get_account(self, account_id): - """ Get information for a single account. - - Use this endpoint when you know the account_id. - - Args: - account_id (str): Account id for account you want to get. - - Returns: - dict: Account information. Example:: - { - "id": "a1b2c3d4", - "balance": "1.100", - "holds": "0.100", - "available": "1.00", - "currency": "USD" - } - """ - return self._send_message('get', '/accounts/' + account_id) - - def get_accounts(self): - """ Get a list of trading all accounts. - - When you place an order, the funds for the order are placed on - hold. They cannot be used for other orders or withdrawn. Funds - will remain on hold until the order is filled or canceled. The - funds on hold for each account will be specified. - - Returns: - list: Info about all accounts. Example:: - [ - { - "id": "71452118-efc7-4cc4-8780-a5e22d4baa53", - "currency": "BTC", - "balance": "0.0000000000000000", - "available": "0.0000000000000000", - "hold": "0.0000000000000000", - "profile_id": "75da88c5-05bf-4f54-bc85-5c775bd68254" - }, - { - ... - } - ] - - * Additional info included in response for margin accounts. - """ - return self.get_account('') - - def get_account_history(self, account_id, **kwargs): - """ List account activity. Account activity either increases or - decreases your account balance. - - Entry type indicates the reason for the account change. - * transfer: Funds moved to/from Coinbase to GDAX - * match: Funds moved as a result of a trade - * fee: Fee as a result of a trade - * rebate: Fee rebate as per our fee schedule - - If an entry is the result of a trade (match, fee), the details - field will contain additional information about the trade. - - Args: - account_id (str): Account id to get history of. - kwargs (dict): Additional HTTP request parameters. - - Returns: - list: History information for the account. Example:: - [ - { - "id": "100", - "created_at": "2014-11-07T08:19:27.028459Z", - "amount": "0.001", - "balance": "239.669", - "type": "fee", - "details": { - "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", - "trade_id": "74", - "product_id": "BTC-USD" - } - }, - { - ... - } - ] - """ - endpoint = '/accounts/{}/ledger'.format(account_id) - return self._send_paginated_message(endpoint, params=kwargs) - - def get_account_holds(self, account_id, **kwargs): - """ Get holds on an account. - - This method returns a generator which may make multiple HTTP requests - while iterating through it. - - Holds are placed on an account for active orders or - pending withdraw requests. - - As an order is filled, the hold amount is updated. If an order - is canceled, any remaining hold is removed. For a withdraw, once - it is completed, the hold is removed. - - The `type` field will indicate why the hold exists. The hold - type is 'order' for holds related to open orders and 'transfer' - for holds related to a withdraw. - - The `ref` field contains the id of the order or transfer which - created the hold. - - Args: - account_id (str): Account id to get holds of. - kwargs (dict): Additional HTTP request parameters. - - Returns: - generator(list): Hold information for the account. Example:: - [ - { - "id": "82dcd140-c3c7-4507-8de4-2c529cd1a28f", - "account_id": "e0b3f39a-183d-453e-b754-0c13e5bab0b3", - "created_at": "2014-11-06T10:34:47.123456Z", - "updated_at": "2014-11-06T10:40:47.123456Z", - "amount": "4.23", - "type": "order", - "ref": "0a205de4-dd35-4370-a285-fe8fc375a273", - }, - { - ... - } - ] - - """ - endpoint = '/accounts/{}/holds'.format(account_id) - return self._send_paginated_message(endpoint, params=kwargs) - - def place_order(self, product_id, side, order_type, **kwargs): - """ Place an order. - - The three order types (limit, market, and stop) can be placed using this - method. Specific methods are provided for each order type, but if a - more generic interface is desired this method is available. - - Args: - product_id (str): Product to order (eg. 'BTC-USD') - side (str): Order side ('buy' or 'sell) - order_type (str): Order type ('limit', 'market', or 'stop') - **client_oid (str): Order ID selected by you to identify your order. - This should be a UUID, which will be broadcast in the public - feed for `received` messages. - **stp (str): Self-trade prevention flag. GDAX doesn't allow self- - trading. This behavior can be modified with this flag. - Options: - 'dc' Decrease and Cancel (default) - 'co' Cancel oldest - 'cn' Cancel newest - 'cb' Cancel both - **overdraft_enabled (Optional[bool]): If true funding above and - beyond the account balance will be provided by margin, as - necessary. - **funding_amount (Optional[Decimal]): Amount of margin funding to be - provided for the order. Mutually exclusive with - `overdraft_enabled`. - **kwargs: Additional arguments can be specified for different order - types. See the limit/market/stop order methods for details. - - Returns: - dict: Order details. Example:: - { - "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2", - "price": "0.10000000", - "size": "0.01000000", - "product_id": "BTC-USD", - "side": "buy", - "stp": "dc", - "type": "limit", - "time_in_force": "GTC", - "post_only": false, - "created_at": "2016-12-08T20:02:28.53864Z", - "fill_fees": "0.0000000000000000", - "filled_size": "0.00000000", - "executed_value": "0.0000000000000000", - "status": "pending", - "settled": false - } - - """ - # Margin parameter checks - if kwargs.get('overdraft_enabled') is not None and \ - kwargs.get('funding_amount') is not None: - raise ValueError('Margin funding must be specified through use of ' - 'overdraft or by setting a funding amount, but not' - ' both') - - # Limit order checks - if order_type == 'limit': - if kwargs.get('cancel_after') is not None and \ - kwargs.get('tif') != 'GTT': - raise ValueError('May only specify a cancel period when time ' - 'in_force is `GTT`') - if kwargs.get('post_only') is not None and kwargs.get('tif') in \ - ['IOC', 'FOK']: - raise ValueError('post_only is invalid when time in force is ' - '`IOC` or `FOK`') - - # Market and stop order checks - if order_type == 'market' or order_type == 'stop': - if not (kwargs.get('size') is None) ^ (kwargs.get('funds') is None): - raise ValueError('Either `size` or `funds` must be specified ' - 'for market/stop orders (but not both).') - - # Build params dict - params = {'product_id': product_id, - 'side': side, - 'type': order_type} - params.update(kwargs) - return self._send_message('post', '/orders', data=json.dumps(params)) - - def buy(self, product_id, order_type, **kwargs): - """Place a buy order. - - This is included to maintain backwards compatibility with older versions - of GDAX-Python. For maximum support from docstrings and function - signatures see the order type-specific functions place_limit_order, - place_market_order, and place_stop_order. - - Args: - product_id (str): Product to order (eg. 'BTC-USD') - order_type (str): Order type ('limit', 'market', or 'stop') - **kwargs: Additional arguments can be specified for different order - types. - - Returns: - dict: Order details. See `place_order` for example. - - """ - return self.place_order(product_id, 'buy', order_type, **kwargs) - - def sell(self, product_id, order_type, **kwargs): - """Place a sell order. - - This is included to maintain backwards compatibility with older versions - of GDAX-Python. For maximum support from docstrings and function - signatures see the order type-specific functions place_limit_order, - place_market_order, and place_stop_order. - - Args: - product_id (str): Product to order (eg. 'BTC-USD') - order_type (str): Order type ('limit', 'market', or 'stop') - **kwargs: Additional arguments can be specified for different order - types. - - Returns: - dict: Order details. See `place_order` for example. - - """ - return self.place_order(product_id, 'sell', order_type, **kwargs) - - def place_limit_order(self, product_id, side, price, size, - client_oid=None, - stp=None, - time_in_force=None, - cancel_after=None, - post_only=None, - overdraft_enabled=None, - funding_amount=None): - """Place a limit order. - - Args: - product_id (str): Product to order (eg. 'BTC-USD') - side (str): Order side ('buy' or 'sell) - price (Decimal): Price per cryptocurrency - size (Decimal): Amount of cryptocurrency to buy or sell - client_oid (Optional[str]): User-specified Order ID - stp (Optional[str]): Self-trade prevention flag. See `place_order` - for details. - time_in_force (Optional[str]): Time in force. Options: - 'GTC' Good till canceled - 'GTT' Good till time (set by `cancel_after`) - 'IOC' Immediate or cancel - 'FOK' Fill or kill - cancel_after (Optional[str]): Cancel after this period for 'GTT' - orders. Options are 'min', 'hour', or 'day'. - post_only (Optional[bool]): Indicates that the order should only - make liquidity. If any part of the order results in taking - liquidity, the order will be rejected and no part of it will - execute. - overdraft_enabled (Optional[bool]): If true funding above and - beyond the account balance will be provided by margin, as - necessary. - funding_amount (Optional[Decimal]): Amount of margin funding to be - provided for the order. Mutually exclusive with - `overdraft_enabled`. - - Returns: - dict: Order details. See `place_order` for example. - - """ - params = {'product_id': product_id, - 'side': side, - 'order_type': 'limit', - 'price': price, - 'size': size, - 'client_oid': client_oid, - 'stp': stp, - 'time_in_force': time_in_force, - 'cancel_after': cancel_after, - 'post_only': post_only, - 'overdraft_enabled': overdraft_enabled, - 'funding_amount': funding_amount} - params = dict((k, v) for k, v in params.items() if v is not None) - - return self.place_order(**params) - - def place_market_order(self, product_id, side, size=None, funds=None, - client_oid=None, - stp=None, - overdraft_enabled=None, - funding_amount=None): - """ Place market order. - - Args: - product_id (str): Product to order (eg. 'BTC-USD') - side (str): Order side ('buy' or 'sell) - size (Optional[Decimal]): Desired amount in crypto. Specify this or - `funds`. - funds (Optional[Decimal]): Desired amount of quote currency to use. - Specify this or `size`. - client_oid (Optional[str]): User-specified Order ID - stp (Optional[str]): Self-trade prevention flag. See `place_order` - for details. - overdraft_enabled (Optional[bool]): If true funding above and - beyond the account balance will be provided by margin, as - necessary. - funding_amount (Optional[Decimal]): Amount of margin funding to be - provided for the order. Mutually exclusive with - `overdraft_enabled`. - - Returns: - dict: Order details. See `place_order` for example. - - """ - params = {'product_id': product_id, - 'side': side, - 'order_type': 'market', - 'size': size, - 'funds': funds, - 'client_oid': client_oid, - 'stp': stp, - 'overdraft_enabled': overdraft_enabled, - 'funding_amount': funding_amount} - params = dict((k, v) for k, v in params.items() if v is not None) - - return self.place_order(**params) - - def place_stop_order(self, product_id, side, price, size=None, funds=None, - client_oid=None, - stp=None, - overdraft_enabled=None, - funding_amount=None): - """ Place stop order. - - Args: - product_id (str): Product to order (eg. 'BTC-USD') - side (str): Order side ('buy' or 'sell) - price (Decimal): Desired price at which the stop order triggers. - size (Optional[Decimal]): Desired amount in crypto. Specify this or - `funds`. - funds (Optional[Decimal]): Desired amount of quote currency to use. - Specify this or `size`. - client_oid (Optional[str]): User-specified Order ID - stp (Optional[str]): Self-trade prevention flag. See `place_order` - for details. - overdraft_enabled (Optional[bool]): If true funding above and - beyond the account balance will be provided by margin, as - necessary. - funding_amount (Optional[Decimal]): Amount of margin funding to be - provided for the order. Mutually exclusive with - `overdraft_enabled`. - - Returns: - dict: Order details. See `place_order` for example. - - """ - params = {'product_id': product_id, - 'side': side, - 'price': price, - 'order_type': 'stop', - 'size': size, - 'funds': funds, - 'client_oid': client_oid, - 'stp': stp, - 'overdraft_enabled': overdraft_enabled, - 'funding_amount': funding_amount} - params = dict((k, v) for k, v in params.items() if v is not None) - - return self.place_order(**params) - - def cancel_order(self, order_id): - """ Cancel a previously placed order. - - If the order had no matches during its lifetime its record may - be purged. This means the order details will not be available - with get_order(order_id). If the order could not be canceled - (already filled or previously canceled, etc), then an error - response will indicate the reason in the message field. - - **Caution**: The order id is the server-assigned order id and - not the optional client_oid. - - Args: - order_id (str): The order_id of the order you want to cancel - - Returns: - list: Containing the order_id of cancelled order. Example:: - [ "c5ab5eae-76be-480e-8961-00792dc7e138" ] - - """ - return self._send_message('delete', '/orders/' + order_id) - - def cancel_all(self, product_id=None): - """ With best effort, cancel all open orders. - - Args: - product_id (Optional[str]): Only cancel orders for this - product_id - - Returns: - list: A list of ids of the canceled orders. Example:: - [ - "144c6f8e-713f-4682-8435-5280fbe8b2b4", - "debe4907-95dc-442f-af3b-cec12f42ebda", - "cf7aceee-7b08-4227-a76c-3858144323ab", - "dfc5ae27-cadb-4c0c-beef-8994936fde8a", - "34fecfbf-de33-4273-b2c6-baf8e8948be4" - ] - - """ - if product_id is not None: - params = {'product_id': product_id} - data = json.dumps(params) - else: - data = None - return self._send_message('delete', '/orders', data=data) - - def get_order(self, order_id): - """ Get a single order by order id. - - If the order is canceled the response may have status code 404 - if the order had no matches. - - **Caution**: Open orders may change state between the request - and the response depending on market conditions. - - Args: - order_id (str): The order to get information of. - - Returns: - dict: Containing information on order. Example:: - { - "created_at": "2017-06-18T00:27:42.920136Z", - "executed_value": "0.0000000000000000", - "fill_fees": "0.0000000000000000", - "filled_size": "0.00000000", - "id": "9456f388-67a9-4316-bad1-330c5353804f", - "post_only": true, - "price": "1.00000000", - "product_id": "BTC-USD", - "settled": false, - "side": "buy", - "size": "1.00000000", - "status": "pending", - "stp": "dc", - "time_in_force": "GTC", - "type": "limit" - } - - """ - return self._send_message('get', '/orders/' + order_id) - - def get_orders(self, product_id=None, status=None, **kwargs): - """ List your current open orders. - - This method returns a generator which may make multiple HTTP requests - while iterating through it. - - Only open or un-settled orders are returned. As soon as an - order is no longer open and settled, it will no longer appear - in the default request. - - Orders which are no longer resting on the order book, will be - marked with the 'done' status. There is a small window between - an order being 'done' and 'settled'. An order is 'settled' when - all of the fills have settled and the remaining holds (if any) - have been removed. - - For high-volume trading it is strongly recommended that you - maintain your own list of open orders and use one of the - streaming market data feeds to keep it updated. You should poll - the open orders endpoint once when you start trading to obtain - the current state of any open orders. - - Args: - product_id (Optional[str]): Only list orders for this - product - status (Optional[list/str]): Limit list of orders to - this status or statuses. Passing 'all' returns orders - of all statuses. - ** Options: 'open', 'pending', 'active', 'done', - 'settled' - ** default: ['open', 'pending', 'active'] - - Returns: - list: Containing information on orders. Example:: - [ - { - "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2", - "price": "0.10000000", - "size": "0.01000000", - "product_id": "BTC-USD", - "side": "buy", - "stp": "dc", - "type": "limit", - "time_in_force": "GTC", - "post_only": false, - "created_at": "2016-12-08T20:02:28.53864Z", - "fill_fees": "0.0000000000000000", - "filled_size": "0.00000000", - "executed_value": "0.0000000000000000", - "status": "open", - "settled": false - }, - { - ... - } - ] - - """ - params = kwargs - if product_id is not None: - params['product_id'] = product_id - if status is not None: - params['status'] = status - return self._send_paginated_message('/orders', params=params) - - def get_fills(self, product_id=None, order_id=None, **kwargs): - """ Get a list of recent fills. - - This method returns a generator which may make multiple HTTP requests - while iterating through it. - - Fees are recorded in two stages. Immediately after the matching - engine completes a match, the fill is inserted into our - datastore. Once the fill is recorded, a settlement process will - settle the fill and credit both trading counterparties. - - The 'fee' field indicates the fees charged for this fill. - - The 'liquidity' field indicates if the fill was the result of a - liquidity provider or liquidity taker. M indicates Maker and T - indicates Taker. - - Args: - product_id (Optional[str]): Limit list to this product_id - order_id (Optional[str]): Limit list to this order_id - kwargs (dict): Additional HTTP request parameters. - - Returns: - list: Containing information on fills. Example:: - [ - { - "trade_id": 74, - "product_id": "BTC-USD", - "price": "10.00", - "size": "0.01", - "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b", - "created_at": "2014-11-07T22:19:28.578544Z", - "liquidity": "T", - "fee": "0.00025", - "settled": true, - "side": "buy" - }, - { - ... - } - ] - - """ - params = {} - if product_id: - params['product_id'] = product_id - if order_id: - params['order_id'] = order_id - params.update(kwargs) - - return self._send_paginated_message('/fills', params=params) - - def get_fundings(self, status=None, **kwargs): - """ Every order placed with a margin profile that draws funding - will create a funding record. - - This method returns a generator which may make multiple HTTP requests - while iterating through it. - - Args: - status (list/str): Limit funding records to these statuses. - ** Options: 'outstanding', 'settled', 'rejected' - kwargs (dict): Additional HTTP request parameters. - - Returns: - list: Containing information on margin funding. Example:: - [ - { - "id": "b93d26cd-7193-4c8d-bfcc-446b2fe18f71", - "order_id": "b93d26cd-7193-4c8d-bfcc-446b2fe18f71", - "profile_id": "d881e5a6-58eb-47cd-b8e2-8d9f2e3ec6f6", - "amount": "1057.6519956381537500", - "status": "settled", - "created_at": "2017-03-17T23:46:16.663397Z", - "currency": "USD", - "repaid_amount": "1057.6519956381537500", - "default_amount": "0", - "repaid_default": false - }, - { - ... - } - ] - - """ - params = {} - if status is not None: - params['status'] = status - params.update(kwargs) - return self._send_paginated_message('/funding', params=params) - - def repay_funding(self, amount, currency): - """ Repay funding. Repays the older funding records first. - - Args: - amount (int): Amount of currency to repay - currency (str): The currency, example USD - - Returns: - Not specified by GDAX. - - """ - params = { - 'amount': amount, - 'currency': currency # example: USD - } - return self._send_message('post', '/funding/repay', - data=json.dumps(params)) - - def margin_transfer(self, margin_profile_id, transfer_type, currency, - amount): - """ Transfer funds between your standard profile and a margin profile. - - Args: - margin_profile_id (str): Margin profile ID to withdraw or deposit - from. - transfer_type (str): 'deposit' or 'withdraw' - currency (str): Currency to transfer (eg. 'USD') - amount (Decimal): Amount to transfer - - Returns: - dict: Transfer details. Example:: - { - "created_at": "2017-01-25T19:06:23.415126Z", - "id": "80bc6b74-8b1f-4c60-a089-c61f9810d4ab", - "user_id": "521c20b3d4ab09621f000011", - "profile_id": "cda95996-ac59-45a3-a42e-30daeb061867", - "margin_profile_id": "45fa9e3b-00ba-4631-b907-8a98cbdf21be", - "type": "deposit", - "amount": "2", - "currency": "USD", - "account_id": "23035fc7-0707-4b59-b0d2-95d0c035f8f5", - "margin_account_id": "e1d9862c-a259-4e83-96cd-376352a9d24d", - "margin_product_id": "BTC-USD", - "status": "completed", - "nonce": 25 - } - - """ - params = {'margin_profile_id': margin_profile_id, - 'type': transfer_type, - 'currency': currency, # example: USD - 'amount': amount} - return self._send_message('post', '/profiles/margin-transfer', - data=json.dumps(params)) - - def get_position(self): - """ Get An overview of your margin profile. - - Returns: - dict: Details about funding, accounts, and margin call. - - """ - return self._send_message('get', '/position') - - def close_position(self, repay_only): - """ Close position. - - Args: - repay_only (bool): Undocumented by GDAX. - - Returns: - Undocumented - - """ - params = {'repay_only': repay_only} - return self._send_message('post', '/position/close', - data=json.dumps(params)) - - def deposit(self, amount, currency, payment_method_id): - """ Deposit funds from a payment method. - - See AuthenticatedClient.get_payment_methods() to receive - information regarding payment methods. - - Args: - amount (Decmial): The amount to deposit. - currency (str): The type of currency. - payment_method_id (str): ID of the payment method. - - Returns: - dict: Information about the deposit. Example:: - { - "id": "593533d2-ff31-46e0-b22e-ca754147a96a", - "amount": "10.00", - "currency": "USD", - "payout_at": "2016-08-20T00:31:09Z" - } - - """ - params = {'amount': amount, - 'currency': currency, - 'payment_method_id': payment_method_id} - return self._send_message('post', '/deposits/payment-method', - data=json.dumps(params)) - - def coinbase_deposit(self, amount, currency, coinbase_account_id): - """ Deposit funds from a coinbase account. - - You can move funds between your Coinbase accounts and your GDAX - trading accounts within your daily limits. Moving funds between - Coinbase and GDAX is instant and free. - - See AuthenticatedClient.get_coinbase_accounts() to receive - information regarding your coinbase_accounts. - - Args: - amount (Decimal): The amount to deposit. - currency (str): The type of currency. - coinbase_account_id (str): ID of the coinbase account. - - Returns: - dict: Information about the deposit. Example:: - { - "id": "593533d2-ff31-46e0-b22e-ca754147a96a", - "amount": "10.00", - "currency": "BTC", - } - - """ - params = {'amount': amount, - 'currency': currency, - 'coinbase_account_id': coinbase_account_id} - return self._send_message('post', '/deposits/coinbase-account', - data=json.dumps(params)) - - def withdraw(self, amount, currency, payment_method_id): - """ Withdraw funds to a payment method. - - See AuthenticatedClient.get_payment_methods() to receive - information regarding payment methods. - - Args: - amount (Decimal): The amount to withdraw. - currency (str): Currency type (eg. 'BTC') - payment_method_id (str): ID of the payment method. - - Returns: - dict: Withdraw details. Example:: - { - "id":"593533d2-ff31-46e0-b22e-ca754147a96a", - "amount": "10.00", - "currency": "USD", - "payout_at": "2016-08-20T00:31:09Z" - } - - """ - params = {'amount': amount, - 'currency': currency, - 'payment_method_id': payment_method_id} - return self._send_message('post', '/withdrawals/payment-method', - data=json.dumps(params)) - - def coinbase_withdraw(self, amount, currency, coinbase_account_id): - """ Withdraw funds to a coinbase account. - - You can move funds between your Coinbase accounts and your GDAX - trading accounts within your daily limits. Moving funds between - Coinbase and GDAX is instant and free. - - See AuthenticatedClient.get_coinbase_accounts() to receive - information regarding your coinbase_accounts. - - Args: - amount (Decimal): The amount to withdraw. - currency (str): The type of currency (eg. 'BTC') - coinbase_account_id (str): ID of the coinbase account. - - Returns: - dict: Information about the deposit. Example:: - { - "id":"593533d2-ff31-46e0-b22e-ca754147a96a", - "amount":"10.00", - "currency": "BTC", - } - - """ - params = {'amount': amount, - 'currency': currency, - 'coinbase_account_id': coinbase_account_id} - return self._send_message('post', '/withdrawals/coinbase', - data=json.dumps(params)) - - def crypto_withdraw(self, amount, currency, crypto_address): - """ Withdraw funds to a crypto address. - - Args: - amount (Decimal): The amount to withdraw - currency (str): The type of currency (eg. 'BTC') - crypto_address (str): Crypto address to withdraw to. - - Returns: - dict: Withdraw details. Example:: - { - "id":"593533d2-ff31-46e0-b22e-ca754147a96a", - "amount":"10.00", - "currency": "BTC", - } - - """ - params = {'amount': amount, - 'currency': currency, - 'crypto_address': crypto_address} - return self._send_message('post', '/withdrawals/crypto', - data=json.dumps(params)) - - def get_payment_methods(self): - """ Get a list of your payment methods. - - Returns: - list: Payment method details. - - """ - return self._send_message('get', '/payment-methods') - - def get_coinbase_accounts(self): - """ Get a list of your coinbase accounts. - - Returns: - list: Coinbase account details. - - """ - return self._send_message('get', '/coinbase-accounts') - - def create_report(self, report_type, start_date, end_date, product_id=None, - account_id=None, report_format='pdf', email=None): - """ Create report of historic information about your account. - - The report will be generated when resources are available. Report status - can be queried via `get_report(report_id)`. - - Args: - report_type (str): 'fills' or 'account' - start_date (str): Starting date for the report in ISO 8601 - end_date (str): Ending date for the report in ISO 8601 - product_id (Optional[str]): ID of the product to generate a fills - report for. Required if account_type is 'fills' - account_id (Optional[str]): ID of the account to generate an account - report for. Required if report_type is 'account'. - report_format (Optional[str]): 'pdf' or 'csv'. Default is 'pdf'. - email (Optional[str]): Email address to send the report to. - - Returns: - dict: Report details. Example:: - { - "id": "0428b97b-bec1-429e-a94c-59232926778d", - "type": "fills", - "status": "pending", - "created_at": "2015-01-06T10:34:47.000Z", - "completed_at": undefined, - "expires_at": "2015-01-13T10:35:47.000Z", - "file_url": undefined, - "params": { - "start_date": "2014-11-01T00:00:00.000Z", - "end_date": "2014-11-30T23:59:59.000Z" - } - } - - """ - params = {'type': report_type, - 'start_date': start_date, - 'end_date': end_date, - 'format': report_format} - if product_id is not None: - params['product_id'] = product_id - if account_id is not None: - params['account_id'] = account_id - if email is not None: - params['email'] = email - - return self._send_message('post', '/reports', - data=json.dumps(params)) - - def get_report(self, report_id): - """ Get report status. - - Use to query a specific report once it has been requested. - - Args: - report_id (str): Report ID - - Returns: - dict: Report details, including file url once it is created. - - """ - return self._send_message('get', '/reports/' + report_id) - - def get_trailing_volume(self): - """ Get your 30-day trailing volume for all products. - - This is a cached value that's calculated every day at midnight UTC. - - Returns: - list: 30-day trailing volumes. Example:: - [ - { - "product_id": "BTC-USD", - "exchange_volume": "11800.00000000", - "volume": "100.00000000", - "recorded_at": "1973-11-29T00:05:01.123456Z" - }, - { - ... - } - ] - - """ - return self._send_message('get', '/users/self/trailing-volume') - - -class GdaxAuth(AuthBase): - # Provided by gdax: https://docs.gdax.com/#signing-a-message - def __init__(self, api_key, secret_key, passphrase): - self.api_key = api_key - self.secret_key = secret_key - self.passphrase = passphrase - - def __call__(self, request): - timestamp = str(time.time()) - message = timestamp + request.method + request.path_url + \ - (request.body or '') - message = message.encode('ascii') - hmac_key = base64.b64decode(self.secret_key) - signature = hmac.new(hmac_key, message, hashlib.sha256) - signature_b64 = base64.b64encode(signature.digest()) - request.headers.update({ - 'Content-Type': 'Application/json', - 'CB-ACCESS-SIGN': signature_b64, - 'CB-ACCESS-TIMESTAMP': timestamp, - 'CB-ACCESS-KEY': self.api_key, - 'CB-ACCESS-PASSPHRASE': self.passphrase - }) - return request diff --git a/gdax/gdax_auth.py b/gdax/gdax_auth.py deleted file mode 100644 index c4217331..00000000 --- a/gdax/gdax_auth.py +++ /dev/null @@ -1,37 +0,0 @@ -import hmac -import hashlib -import time -import base64 -from requests.auth import AuthBase - - -class GdaxAuth(AuthBase): - # Provided by gdax: https://docs.gdax.com/#signing-a-message - def __init__(self, api_key, secret_key, passphrase): - self.api_key = api_key - self.secret_key = secret_key - self.passphrase = passphrase - - def __call__(self, request): - timestamp = str(time.time()) - message = ''.join([timestamp, request.method, - request.path_url, (request.body or '')]) - request.headers.update(get_auth_headers(timestamp, message, - self.api_key, - self.secret_key, - self.passphrase)) - return request - - -def get_auth_headers(timestamp, message, api_key, secret_key, passphrase): - message = message.encode('ascii') - hmac_key = base64.b64decode(secret_key) - signature = hmac.new(hmac_key, message, hashlib.sha256) - signature_b64 = base64.b64encode(signature.digest()).decode('utf-8') - return { - 'Content-Type': 'Application/JSON', - 'CB-ACCESS-SIGN': signature_b64, - 'CB-ACCESS-TIMESTAMP': timestamp, - 'CB-ACCESS-KEY': api_key, - 'CB-ACCESS-PASSPHRASE': passphrase - } diff --git a/gdax/order_book.py b/gdax/order_book.py deleted file mode 100644 index d3ea56eb..00000000 --- a/gdax/order_book.py +++ /dev/null @@ -1,298 +0,0 @@ -# -# gdax/order_book.py -# David Caseria -# -# Live order book updated from the gdax Websocket Feed - -from sortedcontainers import SortedDict -from decimal import Decimal -import pickle - -from gdax.public_client import PublicClient -from gdax.websocket_client import WebsocketClient - - -class OrderBook(WebsocketClient): - def __init__(self, product_id='BTC-USD', log_to=None): - super(OrderBook, self).__init__(products=product_id) - self._asks = SortedDict() - self._bids = SortedDict() - self._client = PublicClient() - self._sequence = -1 - self._log_to = log_to - if self._log_to: - assert hasattr(self._log_to, 'write') - self._current_ticker = None - - @property - def product_id(self): - ''' Currently OrderBook only supports a single product even though it is stored as a list of products. ''' - return self.products[0] - - def on_open(self): - self._sequence = -1 - print("-- Subscribed to OrderBook! --\n") - - def on_close(self): - print("\n-- OrderBook Socket Closed! --") - - def reset_book(self): - self._asks = SortedDict() - self._bids = SortedDict() - res = self._client.get_product_order_book(product_id=self.product_id, level=3) - for bid in res['bids']: - self.add({ - 'id': bid[2], - 'side': 'buy', - 'price': Decimal(bid[0]), - 'size': Decimal(bid[1]) - }) - for ask in res['asks']: - self.add({ - 'id': ask[2], - 'side': 'sell', - 'price': Decimal(ask[0]), - 'size': Decimal(ask[1]) - }) - self._sequence = res['sequence'] - - def on_message(self, message): - if self._log_to: - pickle.dump(message, self._log_to) - - sequence = message.get('sequence', -1) - if self._sequence == -1: - self.reset_book() - return - if sequence <= self._sequence: - # ignore older messages (e.g. before order book initialization from getProductOrderBook) - return - elif sequence > self._sequence + 1: - self.on_sequence_gap(self._sequence, sequence) - return - - msg_type = message['type'] - if msg_type == 'open': - self.add(message) - elif msg_type == 'done' and 'price' in message: - self.remove(message) - elif msg_type == 'match': - self.match(message) - self._current_ticker = message - elif msg_type == 'change': - self.change(message) - - self._sequence = sequence - - def on_sequence_gap(self, gap_start, gap_end): - self.reset_book() - print('Error: messages missing ({} - {}). Re-initializing book at sequence.'.format( - gap_start, gap_end, self._sequence)) - - - def add(self, order): - order = { - 'id': order.get('order_id') or order['id'], - 'side': order['side'], - 'price': Decimal(order['price']), - 'size': Decimal(order.get('size') or order['remaining_size']) - } - if order['side'] == 'buy': - bids = self.get_bids(order['price']) - if bids is None: - bids = [order] - else: - bids.append(order) - self.set_bids(order['price'], bids) - else: - asks = self.get_asks(order['price']) - if asks is None: - asks = [order] - else: - asks.append(order) - self.set_asks(order['price'], asks) - - def remove(self, order): - price = Decimal(order['price']) - if order['side'] == 'buy': - bids = self.get_bids(price) - if bids is not None: - bids = [o for o in bids if o['id'] != order['order_id']] - if len(bids) > 0: - self.set_bids(price, bids) - else: - self.remove_bids(price) - else: - asks = self.get_asks(price) - if asks is not None: - asks = [o for o in asks if o['id'] != order['order_id']] - if len(asks) > 0: - self.set_asks(price, asks) - else: - self.remove_asks(price) - - def match(self, order): - size = Decimal(order['size']) - price = Decimal(order['price']) - - if order['side'] == 'buy': - bids = self.get_bids(price) - if not bids: - return - assert bids[0]['id'] == order['maker_order_id'] - if bids[0]['size'] == size: - self.set_bids(price, bids[1:]) - else: - bids[0]['size'] -= size - self.set_bids(price, bids) - else: - asks = self.get_asks(price) - if not asks: - return - assert asks[0]['id'] == order['maker_order_id'] - if asks[0]['size'] == size: - self.set_asks(price, asks[1:]) - else: - asks[0]['size'] -= size - self.set_asks(price, asks) - - def change(self, order): - try: - new_size = Decimal(order['new_size']) - except KeyError: - return - - try: - price = Decimal(order['price']) - except KeyError: - return - - if order['side'] == 'buy': - bids = self.get_bids(price) - if bids is None or not any(o['id'] == order['order_id'] for o in bids): - return - index = [b['id'] for b in bids].index(order['order_id']) - bids[index]['size'] = new_size - self.set_bids(price, bids) - else: - asks = self.get_asks(price) - if asks is None or not any(o['id'] == order['order_id'] for o in asks): - return - index = [a['id'] for a in asks].index(order['order_id']) - asks[index]['size'] = new_size - self.set_asks(price, asks) - - tree = self._asks if order['side'] == 'sell' else self._bids - node = tree.get(price) - - if node is None or not any(o['id'] == order['order_id'] for o in node): - return - - def get_current_ticker(self): - return self._current_ticker - - def get_current_book(self): - result = { - 'sequence': self._sequence, - 'asks': [], - 'bids': [], - } - for ask in self._asks: - try: - # There can be a race condition here, where a price point is removed - # between these two ops - this_ask = self._asks[ask] - except KeyError: - continue - for order in this_ask: - result['asks'].append([order['price'], order['size'], order['id']]) - for bid in self._bids: - try: - # There can be a race condition here, where a price point is removed - # between these two ops - this_bid = self._bids[bid] - except KeyError: - continue - - for order in this_bid: - result['bids'].append([order['price'], order['size'], order['id']]) - return result - - def get_ask(self): - return self._asks.peekitem(0)[0] - - def get_asks(self, price): - return self._asks.get(price) - - def remove_asks(self, price): - del self._asks[price] - - def set_asks(self, price, asks): - self._asks[price] = asks - - def get_bid(self): - return self._bids.peekitem(-1)[0] - - def get_bids(self, price): - return self._bids.get(price) - - def remove_bids(self, price): - del self._bids[price] - - def set_bids(self, price, bids): - self._bids[price] = bids - - -if __name__ == '__main__': - import sys - import time - import datetime as dt - - - class OrderBookConsole(OrderBook): - ''' Logs real-time changes to the bid-ask spread to the console ''' - - def __init__(self, product_id=None): - super(OrderBookConsole, self).__init__(product_id=product_id) - - # latest values of bid-ask spread - self._bid = None - self._ask = None - self._bid_depth = None - self._ask_depth = None - - def on_message(self, message): - super(OrderBookConsole, self).on_message(message) - - # Calculate newest bid-ask spread - bid = self.get_bid() - bids = self.get_bids(bid) - bid_depth = sum([b['size'] for b in bids]) - ask = self.get_ask() - asks = self.get_asks(ask) - ask_depth = sum([a['size'] for a in asks]) - - if self._bid == bid and self._ask == ask and self._bid_depth == bid_depth and self._ask_depth == ask_depth: - # If there are no changes to the bid-ask spread since the last update, no need to print - pass - else: - # If there are differences, update the cache - self._bid = bid - self._ask = ask - self._bid_depth = bid_depth - self._ask_depth = ask_depth - print('{} {} bid: {:.3f} @ {:.2f}\task: {:.3f} @ {:.2f}'.format( - dt.datetime.now(), self.product_id, bid_depth, bid, ask_depth, ask)) - - order_book = OrderBookConsole() - order_book.start() - try: - while True: - time.sleep(10) - except KeyboardInterrupt: - order_book.close() - - if order_book.error: - sys.exit(1) - else: - sys.exit(0) diff --git a/gdax/public_client.py b/gdax/public_client.py deleted file mode 100644 index a1054f4a..00000000 --- a/gdax/public_client.py +++ /dev/null @@ -1,310 +0,0 @@ -# -# GDAX/PublicClient.py -# Daniel Paquin -# -# For public requests to the GDAX exchange - -import requests - - -class PublicClient(object): - """GDAX public client API. - - All requests default to the `product_id` specified at object - creation if not otherwise specified. - - Attributes: - url (Optional[str]): API URL. Defaults to GDAX API. - - """ - - def __init__(self, api_url='https://api.pro.coinbase.com', timeout=30): - """Create GDAX API public client. - - Args: - api_url (Optional[str]): API URL. Defaults to GDAX API. - - """ - self.url = api_url.rstrip('/') - self.auth = None - self.session = requests.Session() - - def get_products(self): - """Get a list of available currency pairs for trading. - - Returns: - list: Info about all currency pairs. Example:: - [ - { - "id": "BTC-USD", - "display_name": "BTC/USD", - "base_currency": "BTC", - "quote_currency": "USD", - "base_min_size": "0.01", - "base_max_size": "10000.00", - "quote_increment": "0.01" - } - ] - - """ - return self._send_message('get', '/products') - - def get_product_order_book(self, product_id, level=1): - """Get a list of open orders for a product. - - The amount of detail shown can be customized with the `level` - parameter: - * 1: Only the best bid and ask - * 2: Top 50 bids and asks (aggregated) - * 3: Full order book (non aggregated) - - Level 1 and Level 2 are recommended for polling. For the most - up-to-date data, consider using the websocket stream. - - **Caution**: Level 3 is only recommended for users wishing to - maintain a full real-time order book using the websocket - stream. Abuse of Level 3 via polling will cause your access to - be limited or blocked. - - Args: - product_id (str): Product - level (Optional[int]): Order book level (1, 2, or 3). - Default is 1. - - Returns: - dict: Order book. Example for level 1:: - { - "sequence": "3", - "bids": [ - [ price, size, num-orders ], - ], - "asks": [ - [ price, size, num-orders ], - ] - } - - """ - params = {'level': level} - return self._send_message('get', - '/products/{}/book'.format(product_id), - params=params) - - def get_product_ticker(self, product_id): - """Snapshot about the last trade (tick), best bid/ask and 24h volume. - - **Caution**: Polling is discouraged in favor of connecting via - the websocket stream and listening for match messages. - - Args: - product_id (str): Product - - Returns: - dict: Ticker info. Example:: - { - "trade_id": 4729088, - "price": "333.99", - "size": "0.193", - "bid": "333.98", - "ask": "333.99", - "volume": "5957.11914015", - "time": "2015-11-14T20:46:03.511254Z" - } - - """ - return self._send_message('get', - '/products/{}/ticker'.format(product_id)) - - def get_product_trades(self, product_id, before='', after='', limit=None, result=None): - """List the latest trades for a product. - - This method returns a generator which may make multiple HTTP requests - while iterating through it. - - Args: - product_id (str): Product - before (Optional[str]): start time in ISO 8601 - after (Optional[str]): end time in ISO 8601 - limit (Optional[int]): the desired number of trades (can be more than 100, - automatically paginated) - results (Optional[list]): list of results that is used for the pagination - Returns: - list: Latest trades. Example:: - [{ - "time": "2014-11-07T22:19:28.578544Z", - "trade_id": 74, - "price": "10.00000000", - "size": "0.01000000", - "side": "buy" - }, { - "time": "2014-11-07T01:08:43.642366Z", - "trade_id": 73, - "price": "100.00000000", - "size": "0.01000000", - "side": "sell" - }] - """ - return self._send_paginated_message('/products/{}/trades' - .format(product_id)) - - def get_product_historic_rates(self, product_id, start=None, end=None, - granularity=None): - """Historic rates for a product. - - Rates are returned in grouped buckets based on requested - `granularity`. If start, end, and granularity aren't provided, - the exchange will assume some (currently unknown) default values. - - Historical rate data may be incomplete. No data is published for - intervals where there are no ticks. - - **Caution**: Historical rates should not be polled frequently. - If you need real-time information, use the trade and book - endpoints along with the websocket feed. - - The maximum number of data points for a single request is 200 - candles. If your selection of start/end time and granularity - will result in more than 200 data points, your request will be - rejected. If you wish to retrieve fine granularity data over a - larger time range, you will need to make multiple requests with - new start/end ranges. - - Args: - product_id (str): Product - start (Optional[str]): Start time in ISO 8601 - end (Optional[str]): End time in ISO 8601 - granularity (Optional[int]): Desired time slice in seconds - - Returns: - list: Historic candle data. Example: - [ - [ time, low, high, open, close, volume ], - [ 1415398768, 0.32, 4.2, 0.35, 4.2, 12.3 ], - ... - ] - - """ - params = {} - if start is not None: - params['start'] = start - if end is not None: - params['end'] = end - if granularity is not None: - acceptedGrans = [60, 300, 900, 3600, 21600, 86400] - if granularity not in acceptedGrans: - raise ValueError( 'Specified granularity is {}, must be in approved values: {}'.format( - granularity, acceptedGrans) ) - - params['granularity'] = granularity - return self._send_message('get', - '/products/{}/candles'.format(product_id)) - - def get_product_24hr_stats(self, product_id): - """Get 24 hr stats for the product. - - Args: - product_id (str): Product - - Returns: - dict: 24 hour stats. Volume is in base currency units. - Open, high, low are in quote currency units. Example:: - { - "open": "34.19000000", - "high": "95.70000000", - "low": "7.06000000", - "volume": "2.41000000" - } - - """ - return self._send_message('get', - '/products/{}/stats'.format(product_id)) - - def get_currencies(self): - """List known currencies. - - Returns: - list: List of currencies. Example:: - [{ - "id": "BTC", - "name": "Bitcoin", - "min_size": "0.00000001" - }, { - "id": "USD", - "name": "United States Dollar", - "min_size": "0.01000000" - }] - - """ - return self._send_message('get', '/currencies') - - def get_time(self): - """Get the API server time. - - Returns: - dict: Server time in ISO and epoch format (decimal seconds - since Unix epoch). Example:: - { - "iso": "2015-01-07T23:47:25.201Z", - "epoch": 1420674445.201 - } - - """ - return self._send_message('get', '/time') - - def _send_message(self, method, endpoint, params=None, data=None): - """Send API request. - - Args: - method (str): HTTP method (get, post, delete, etc.) - endpoint (str): Endpoint (to be added to base URL) - params (Optional[dict]): HTTP request parameters - data (Optional[str]): JSON-encoded string payload for POST - - Returns: - dict/list: JSON response - - """ - url = self.url + endpoint - r = self.session.request(method, url, params=params, data=data, - auth=self.auth, timeout=30) - return r.json() - - def _send_paginated_message(self, endpoint, params=None): - """ Send API message that results in a paginated response. - - The paginated responses are abstracted away by making API requests on - demand as the response is iterated over. - - Paginated API messages support 3 additional parameters: `before`, - `after`, and `limit`. `before` and `after` are mutually exclusive. To - use them, supply an index value for that endpoint (the field used for - indexing varies by endpoint - get_fills() uses 'trade_id', for example). - `before`: Only get data that occurs more recently than index - `after`: Only get data that occurs further in the past than index - `limit`: Set amount of data per HTTP response. Default (and - maximum) of 100. - - Args: - endpoint (str): Endpoint (to be added to base URL) - params (Optional[dict]): HTTP request parameters - - Yields: - dict: API response objects - - """ - if params is None: - params = dict() - url = self.url + endpoint - while True: - r = self.session.get(url, params=params, auth=self.auth, timeout=30) - results = r.json() - for result in results: - yield result - # If there are no more pages, we're done. Otherwise update `after` - # param to get next page. - # If this request included `before` don't get any more pages - the - # GDAX API doesn't support multiple pages in that case. - if not r.headers.get('cb-after') or \ - params.get('before') is not None: - break - else: - params['after'] = r.headers['cb-after'] diff --git a/gdax/websocket_client.py b/gdax/websocket_client.py deleted file mode 100644 index 47000b12..00000000 --- a/gdax/websocket_client.py +++ /dev/null @@ -1,163 +0,0 @@ -# gdax/WebsocketClient.py -# original author: Daniel Paquin -# mongo "support" added by Drew Rice -# -# -# Template object to receive messages from the gdax Websocket Feed - -from __future__ import print_function -import json -import base64 -import hmac -import hashlib -import time -from threading import Thread -from websocket import create_connection, WebSocketConnectionClosedException -from pymongo import MongoClient -from gdax.gdax_auth import get_auth_headers - - -class WebsocketClient(object): - def __init__(self, url="wss://ws-feed.gdax.com", products=None, message_type="subscribe", mongo_collection=None, - should_print=True, auth=False, api_key="", api_secret="", api_passphrase="", channels=None): - self.url = url - self.products = products - self.channels = channels - self.type = message_type - self.stop = False - self.error = None - self.ws = None - self.thread = None - self.auth = auth - self.api_key = api_key - self.api_secret = api_secret - self.api_passphrase = api_passphrase - self.should_print = should_print - self.mongo_collection = mongo_collection - - def start(self): - def _go(): - self._connect() - self._listen() - self._disconnect() - - self.stop = False - self.on_open() - self.thread = Thread(target=_go) - self.thread.start() - - def _connect(self): - if self.products is None: - self.products = ["BTC-USD"] - elif not isinstance(self.products, list): - self.products = [self.products] - - if self.url[-1] == "/": - self.url = self.url[:-1] - - if self.channels is None: - sub_params = {'type': 'subscribe', 'product_ids': self.products} - else: - sub_params = {'type': 'subscribe', 'product_ids': self.products, 'channels': self.channels} - - if self.auth: - timestamp = str(time.time()) - message = timestamp + 'GET' + '/users/self/verify' - message = message.encode('ascii') - hmac_key = base64.b64decode(self.api_secret) - signature = hmac.new(hmac_key, message, hashlib.sha256) - signature_b64 = base64.b64encode(signature.digest()).decode('utf-8').rstrip('\n') - sub_params['signature'] = signature_b64 - sub_params['key'] = self.api_key - sub_params['passphrase'] = self.api_passphrase - sub_params['timestamp'] = timestamp - - self.ws = create_connection(self.url) - - self.ws.send(json.dumps(sub_params)) - - def _listen(self): - while not self.stop: - try: - start_t = 0 - if time.time() - start_t >= 30: - # Set a 30 second ping to keep connection alive - self.ws.ping("keepalive") - start_t = time.time() - data = self.ws.recv() - msg = json.loads(data) - except ValueError as e: - self.on_error(e) - except Exception as e: - self.on_error(e) - else: - self.on_message(msg) - - def _disconnect(self): - try: - if self.ws: - self.ws.close() - except WebSocketConnectionClosedException as e: - pass - - self.on_close() - - def close(self): - self.stop = True - self.thread.join() - - def on_open(self): - if self.should_print: - print("-- Subscribed! --\n") - - def on_close(self): - if self.should_print: - print("\n-- Socket Closed --") - - def on_message(self, msg): - if self.should_print: - print(msg) - if self.mongo_collection: # dump JSON to given mongo collection - self.mongo_collection.insert_one(msg) - - def on_error(self, e, data=None): - self.error = e - self.stop = True - print('{} - data: {}'.format(e, data)) - - -if __name__ == "__main__": - import sys - import gdax - import time - - - class MyWebsocketClient(gdax.WebsocketClient): - def on_open(self): - self.url = "wss://ws-feed.gdax.com/" - self.products = ["BTC-USD", "ETH-USD"] - self.message_count = 0 - print("Let's count the messages!") - - def on_message(self, msg): - print(json.dumps(msg, indent=4, sort_keys=True)) - self.message_count += 1 - - def on_close(self): - print("-- Goodbye! --") - - - wsClient = MyWebsocketClient() - wsClient.start() - print(wsClient.url, wsClient.products) - try: - while True: - print("\nMessageCount =", "%i \n" % wsClient.message_count) - time.sleep(1) - except KeyboardInterrupt: - wsClient.close() - - if wsClient.error: - sys.exit(1) - else: - sys.exit(0) From 75140cfe069fa451959e9b9b8ae01dc49a4b67f2 Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Sun, 19 Aug 2018 19:21:41 -0400 Subject: [PATCH 126/174] modified reference in cbpro/order_book --- cbpro/order_book.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cbpro/order_book.py b/cbpro/order_book.py index d3ea56eb..790b7fce 100644 --- a/cbpro/order_book.py +++ b/cbpro/order_book.py @@ -1,15 +1,15 @@ # -# gdax/order_book.py +# cbpro/order_book.py # David Caseria # -# Live order book updated from the gdax Websocket Feed +# Live order book updated from the Coinbase Websocket Feed from sortedcontainers import SortedDict from decimal import Decimal import pickle -from gdax.public_client import PublicClient -from gdax.websocket_client import WebsocketClient +from cbpro.public_client import PublicClient +from cbpro.websocket_client import WebsocketClient class OrderBook(WebsocketClient): From 7fc92bec07af1b395bee441afa1328c0bc3d7b44 Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Sun, 19 Aug 2018 19:23:54 -0400 Subject: [PATCH 127/174] updated gdax references in tests/ --- tests/test_authenticated_client.py | 8 ++++---- tests/test_public_client.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_authenticated_client.py b/tests/test_authenticated_client.py index 96f034f7..4cfb4f5a 100644 --- a/tests/test_authenticated_client.py +++ b/tests/test_authenticated_client.py @@ -2,13 +2,13 @@ import json import time from itertools import islice -import gdax +import cbpro @pytest.fixture(scope='module') def dc(): """Dummy client for testing.""" - return gdax.AuthenticatedClient('test', 'test', 'test') + return cbpro.AuthenticatedClient('test', 'test', 'test') @pytest.mark.usefixtures('dc') @@ -45,8 +45,8 @@ def client(): provided in api_config.json""" with open('api_config.json') as file: api_config = json.load(file) - c = gdax.AuthenticatedClient( - api_url='https://api-public.sandbox.gdax.com', **api_config) + c = cbpro.AuthenticatedClient( + api_url='https://api-public.sandbox.pro.coinbase.com', **api_config) # Set up account with deposits and orders. Do this by depositing from # the Coinbase USD wallet, which has a fixed value of > $10,000. diff --git a/tests/test_public_client.py b/tests/test_public_client.py index 81737cd5..b07faae1 100644 --- a/tests/test_public_client.py +++ b/tests/test_public_client.py @@ -1,6 +1,6 @@ import pytest from itertools import islice -import gdax +import cbpro import time import datetime from dateutil.relativedelta import relativedelta @@ -8,7 +8,7 @@ @pytest.fixture(scope='module') def client(): - return gdax.PublicClient() + return cbpro.PublicClient() @pytest.mark.usefixtures('client') From a32d5feebfa6265b8e8e993d25bcebd8470fd2ac Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Sun, 19 Aug 2018 19:26:14 -0400 Subject: [PATCH 128/174] updated .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7dcfef5a..77160174 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +*.DS_Store .idea *.pyc example.py @@ -7,7 +8,7 @@ dist/ *.rst venv/ *log.txt -gdax/__pycache__/ +cbpro/__pycache__/ .cache/ .coverage tests/__pycache__/ From 2f28abbdeeaa60932a24a93c6a7236e09da493ca Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Sun, 19 Aug 2018 19:34:42 -0400 Subject: [PATCH 129/174] released cbpro 1.1.1 --- README.md | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 77c26e89..0f608130 100644 --- a/README.md +++ b/README.md @@ -363,11 +363,11 @@ python -m pytest ``` ## Change Log -*1.1* +*1.1.1* **Current PyPI release** - Refactor project for Coinbase Pro - Major overhaul on how pagination is handled -*1.0* **Current PyPI release** +*1.0* - The first release that is not backwards compatible - Refactored to follow PEP 8 Standards - Improved Documentation diff --git a/setup.py b/setup.py index e68cbcc6..30b21637 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ setup( name='cbpro', - version='1.1.0', + version='1.1.1', author='Daniel Paquin', author_email='dpaq34@gmail.com', license='MIT', From fa9edb937001a9db7bdb525b178ccd0c9eeec2ec Mon Sep 17 00:00:00 2001 From: "Thomas Chen, ASA" Date: Tue, 21 Aug 2018 20:54:32 +0900 Subject: [PATCH 130/174] Bug fix: params were not being sent to coinbase api. The call to _send_message was missing the params so the function was not respecting the datetime and granularity parameters when requesting data from coinbase api. So the request would always return whatever data coinbase decided to give to you. --- cbpro/public_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cbpro/public_client.py b/cbpro/public_client.py index 52528964..e854a163 100644 --- a/cbpro/public_client.py +++ b/cbpro/public_client.py @@ -196,7 +196,8 @@ def get_product_historic_rates(self, product_id, start=None, end=None, params['granularity'] = granularity return self._send_message('get', - '/products/{}/candles'.format(product_id)) + '/products/{}/candles'.format(product_id), + params=params) def get_product_24hr_stats(self, product_id): """Get 24 hr stats for the product. From d69535e679cfb815484849e8d5e31b7a738a1d8b Mon Sep 17 00:00:00 2001 From: "Thomas Chen, ASA" Date: Tue, 21 Aug 2018 21:03:20 +0900 Subject: [PATCH 131/174] Websockets are in a stopped state at initialization. The self.stop variable should be True at initialization. --- cbpro/websocket_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cbpro/websocket_client.py b/cbpro/websocket_client.py index 861b7444..fc18c50e 100644 --- a/cbpro/websocket_client.py +++ b/cbpro/websocket_client.py @@ -24,7 +24,7 @@ def __init__(self, url="wss://ws-feed.pro.coinbase.com", products=None, message_ self.products = products self.channels = channels self.type = message_type - self.stop = False + self.stop = True self.error = None self.ws = None self.thread = None From 2ece987d6ba60d003e680d9abf4d3651b604b013 Mon Sep 17 00:00:00 2001 From: alimcmaster1 Date: Wed, 22 Aug 2018 23:42:08 +0100 Subject: [PATCH 132/174] Configure travis CI --- .travis.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..9fad9096 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +sudo: false +language: python +# cache package wheels (1 cache per python version) +cache: pip +python: 3.5 + +install: + - pip install -r requirements.txt + +script: + - pytest From e98e6065a1debb509691c891337bd02577f4de59 Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Thu, 23 Aug 2018 21:11:42 -0400 Subject: [PATCH 133/174] fixed tests references --- tests/test_authenticated_client.py | 6 +++--- tests/test_public_client.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_authenticated_client.py b/tests/test_authenticated_client.py index 4cfb4f5a..e7d574ec 100644 --- a/tests/test_authenticated_client.py +++ b/tests/test_authenticated_client.py @@ -2,13 +2,13 @@ import json import time from itertools import islice -import cbpro +from cbpro.authenticated_client import AuthenticatedClient @pytest.fixture(scope='module') def dc(): """Dummy client for testing.""" - return cbpro.AuthenticatedClient('test', 'test', 'test') + return AuthenticatedClient('test', 'test', 'test') @pytest.mark.usefixtures('dc') @@ -45,7 +45,7 @@ def client(): provided in api_config.json""" with open('api_config.json') as file: api_config = json.load(file) - c = cbpro.AuthenticatedClient( + c = AuthenticatedClient( api_url='https://api-public.sandbox.pro.coinbase.com', **api_config) # Set up account with deposits and orders. Do this by depositing from diff --git a/tests/test_public_client.py b/tests/test_public_client.py index b07faae1..10d2d1f7 100644 --- a/tests/test_public_client.py +++ b/tests/test_public_client.py @@ -1,14 +1,14 @@ import pytest -from itertools import islice -import cbpro import time +from itertools import islice import datetime from dateutil.relativedelta import relativedelta +from cbpro.public_client import PublicClient @pytest.fixture(scope='module') def client(): - return cbpro.PublicClient() + return PublicClient() @pytest.mark.usefixtures('client') From 9ccbc97652db1b7e6c7888b783722eee9f438104 Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Thu, 23 Aug 2018 21:36:01 -0400 Subject: [PATCH 134/174] make cbpro visible to tests --- __init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/__init__.py b/__init__.py index e69de29b..a4dc3090 100644 --- a/__init__.py +++ b/__init__.py @@ -0,0 +1,3 @@ +# for tests +from cbpro.authenticated_client import AuthenticatedClient +from cbpro.public_client import PublicClient From 7121b1fc26121540eb6b4478c0649fc68a7c109e Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Thu, 23 Aug 2018 21:59:07 -0400 Subject: [PATCH 135/174] updated testing environment --- __init__.py | 3 --- requirements.txt | 2 -- 2 files changed, 5 deletions(-) diff --git a/__init__.py b/__init__.py index a4dc3090..e69de29b 100644 --- a/__init__.py +++ b/__init__.py @@ -1,3 +0,0 @@ -# for tests -from cbpro.authenticated_client import AuthenticatedClient -from cbpro.public_client import PublicClient diff --git a/requirements.txt b/requirements.txt index 923b4555..a8378e03 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,3 @@ six==1.10.0 websocket-client==0.40.0 pymongo==3.5.1 pytest>=3.3.0 -pytest-cov>=2.5.0 -py-dateutil==2.2 From 722b36ba35311be80432c9f159cc11068eae3f58 Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Thu, 23 Aug 2018 22:11:45 -0400 Subject: [PATCH 136/174] updated travis install --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9fad9096..510a0621 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ cache: pip python: 3.5 install: - - pip install -r requirements.txt + - python setup.py test script: - pytest From 20ab7af4c6d02fdd810da148d356a4e544df1150 Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Thu, 23 Aug 2018 22:21:52 -0400 Subject: [PATCH 137/174] updated travis install --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 510a0621..3c6d78f1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ cache: pip python: 3.5 install: - - python setup.py test + - pip install git+git://github.com/danpaquin/coinbasepro-python.git script: - pytest From 4e5d7d793bbfebbf988f65c002ca26515ea79f66 Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Thu, 23 Aug 2018 22:24:08 -0400 Subject: [PATCH 138/174] reverted travis install --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3c6d78f1..9fad9096 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ cache: pip python: 3.5 install: - - pip install git+git://github.com/danpaquin/coinbasepro-python.git + - pip install -r requirements.txt script: - pytest From e14144633bf6b9f8cce746150c15e772de7d7d70 Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Sat, 25 Aug 2018 17:32:01 -0400 Subject: [PATCH 139/174] updated websocket.sub_params mapping --- cbpro/websocket_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cbpro/websocket_client.py b/cbpro/websocket_client.py index fc18c50e..b598019f 100644 --- a/cbpro/websocket_client.py +++ b/cbpro/websocket_client.py @@ -65,8 +65,8 @@ def _connect(self): message = timestamp + 'GET' + '/users/self/verify' auth_headers = get_auth_headers(timestamp, message, self.api_key, self.api_secret, self.api_passphrase) sub_params['signature'] = auth_headers['CB-ACCESS-SIGN'] - sub_params['key'] = auth_headers['CB-ACCESS-SIGN'] - sub_params['passphrase'] = auth_headers['CB-ACCESS-KEY'] + sub_params['key'] = auth_headers['CB-ACCESS-KEY'] + sub_params['passphrase'] = auth_headers['CB-ACCESS-PASSPHRASE'] sub_params['timestamp'] = auth_headers['CB-ACCESS-TIMESTAMP'] self.ws = create_connection(self.url) From 5669b608c91bb3a0504fe31ca59d92d4f8f38544 Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Sun, 26 Aug 2018 10:58:11 -0400 Subject: [PATCH 140/174] pip update 1.1.2 --- README.md | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0f608130..cb24f291 100644 --- a/README.md +++ b/README.md @@ -363,7 +363,7 @@ python -m pytest ``` ## Change Log -*1.1.1* **Current PyPI release** +*1.1.2* **Current PyPI release** - Refactor project for Coinbase Pro - Major overhaul on how pagination is handled diff --git a/setup.py b/setup.py index 30b21637..da1dcdc0 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ setup( name='cbpro', - version='1.1.1', + version='1.1.2', author='Daniel Paquin', author_email='dpaq34@gmail.com', license='MIT', From 7d993fc5cdacbc45e04a54653c9df90105549e76 Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Sun, 26 Aug 2018 16:50:45 -0400 Subject: [PATCH 141/174] raise error on get_fills --- cbpro/authenticated_client.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cbpro/authenticated_client.py b/cbpro/authenticated_client.py index f45f31f5..42610cbe 100644 --- a/cbpro/authenticated_client.py +++ b/cbpro/authenticated_client.py @@ -585,6 +585,9 @@ def get_orders(self, product_id=None, status=None, **kwargs): def get_fills(self, product_id=None, order_id=None, **kwargs): """ Get a list of recent fills. + As of 8/23/18 - Requests without either order_id or product_id + will be rejected + This method returns a generator which may make multiple HTTP requests while iterating through it. @@ -599,9 +602,6 @@ def get_fills(self, product_id=None, order_id=None, **kwargs): liquidity provider or liquidity taker. M indicates Maker and T indicates Taker. - As of 8/23/18 - Requests without either order_id or product_id - will be rejected - Args: product_id (str): Limit list to this product_id order_id (str): Limit list to this order_id @@ -628,6 +628,9 @@ def get_fills(self, product_id=None, order_id=None, **kwargs): ] """ + if (product_id is None) and (order_id is None): + raise ValueError('Either product_id or order_id must be specified.') + params = {} if product_id: params['product_id'] = product_id From f6daf37cb36ebbd311ad60164b61d93ff68aab10 Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Sun, 26 Aug 2018 17:01:15 -0400 Subject: [PATCH 142/174] drop JSON encoding for DELETE/cancel_all param functionality --- cbpro/authenticated_client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cbpro/authenticated_client.py b/cbpro/authenticated_client.py index 42610cbe..862bccd0 100644 --- a/cbpro/authenticated_client.py +++ b/cbpro/authenticated_client.py @@ -477,10 +477,9 @@ def cancel_all(self, product_id=None): """ if product_id is not None: params = {'product_id': product_id} - data = json.dumps(params) else: - data = None - return self._send_message('delete', '/orders', data=data) + params = None + return self._send_message('delete', '/orders', params=params) def get_order(self, order_id): """ Get a single order by order id. From 6af1519cbf0d7aaf6acb947d462c16cabe5a8192 Mon Sep 17 00:00:00 2001 From: Chris B Date: Mon, 27 Aug 2018 12:08:59 +0200 Subject: [PATCH 143/174] updated authenticated_client.py For place_order method 'time_in_force' was written as 'tif' - which is not a standard argument for the coinbase pro API. --- cbpro/authenticated_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cbpro/authenticated_client.py b/cbpro/authenticated_client.py index 862bccd0..f8a9152f 100644 --- a/cbpro/authenticated_client.py +++ b/cbpro/authenticated_client.py @@ -234,10 +234,10 @@ def place_order(self, product_id, side, order_type, **kwargs): # Limit order checks if order_type == 'limit': if kwargs.get('cancel_after') is not None and \ - kwargs.get('tif') != 'GTT': + kwargs.get('time_in_force') != 'GTT': raise ValueError('May only specify a cancel period when time ' 'in_force is `GTT`') - if kwargs.get('post_only') is not None and kwargs.get('tif') in \ + if kwargs.get('post_only') is not None and kwargs.get('time_in_force') in \ ['IOC', 'FOK']: raise ValueError('post_only is invalid when time in force is ' '`IOC` or `FOK`') From 9860e8d435897c2e154d0cb1c2d1baff0e9d9efe Mon Sep 17 00:00:00 2001 From: alimcmaster1 Date: Mon, 27 Aug 2018 21:57:15 +0100 Subject: [PATCH 144/174] Get CI tests workings --- .coveragerc | 8 ++++++++ .travis.yml | 4 ++-- pytest.ini | 3 --- requirements.txt => requirements-dev.txt | 2 ++ tests/test_authenticated_client.py | 4 +++- tox.ini | 10 ++++++++++ 6 files changed, 25 insertions(+), 6 deletions(-) create mode 100644 .coveragerc delete mode 100644 pytest.ini rename requirements.txt => requirements-dev.txt (74%) create mode 100644 tox.ini diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..84bb5aca --- /dev/null +++ b/.coveragerc @@ -0,0 +1,8 @@ +[run] +branch = True +source = + . +omit = + .tox/* + setup.py + tests/* diff --git a/.travis.yml b/.travis.yml index 9fad9096..3fa7df95 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ cache: pip python: 3.5 install: - - pip install -r requirements.txt + - python setup.py install script: - - pytest + - python -m pytest tests/ diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 094d739e..00000000 --- a/pytest.ini +++ /dev/null @@ -1,3 +0,0 @@ -[pytest] -addopts = --cov cbpro/ --cov-report=term-missing -testpaths = tests \ No newline at end of file diff --git a/requirements.txt b/requirements-dev.txt similarity index 74% rename from requirements.txt rename to requirements-dev.txt index a8378e03..c01f220d 100644 --- a/requirements.txt +++ b/requirements-dev.txt @@ -4,3 +4,5 @@ six==1.10.0 websocket-client==0.40.0 pymongo==3.5.1 pytest>=3.3.0 +pytest>=3.3.0 +python-dateutil>=2.7.3 \ No newline at end of file diff --git a/tests/test_authenticated_client.py b/tests/test_authenticated_client.py index e7d574ec..ebe10497 100644 --- a/tests/test_authenticated_client.py +++ b/tests/test_authenticated_client.py @@ -23,6 +23,7 @@ def test_place_order_input_2(self, dc): r = dc.place_order('BTC-USD', 'buy', 'limit', cancel_after='123', tif='ABC') + @pytest.mark.skip("Needs fixing") def test_place_order_input_3(self, dc): with pytest.raises(ValueError): r = dc.place_order('BTC-USD', 'buy', 'limit', @@ -43,7 +44,7 @@ def test_place_order_input_5(self, dc): def client(): """Client that connects to sandbox API. Relies on authentication information provided in api_config.json""" - with open('api_config.json') as file: + with open('api_config.json.example') as file: api_config = json.load(file) c = AuthenticatedClient( api_url='https://api-public.sandbox.pro.coinbase.com', **api_config) @@ -68,6 +69,7 @@ def client(): @pytest.mark.usefixtures('dc') +@pytest.mark.skip(reason="these test require authentication") class TestAuthenticatedClient(object): """Test the authenticated client by validating basic behavior from the sandbox exchange.""" diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..e6ca111b --- /dev/null +++ b/tox.ini @@ -0,0 +1,10 @@ +[tox] +envlist = py27, py35, py36 + +[testenv] +setenv = PYTHONPATH = . +deps = + -rrequirements-dev.txt +commands= + python -m pytest -m "not xfail" {posargs: "{toxinidir}/cbpro/tests" --cov-config="{toxinidir}/tox.ini" --cov=cbpro} + python -m pytest -m "xfail" {posargs: "{toxinidir}/cbpro/tests" From 6edaaeb635758b5fbb0f68b94e5c8b106706048d Mon Sep 17 00:00:00 2001 From: alimcmaster1 Date: Mon, 27 Aug 2018 22:02:12 +0100 Subject: [PATCH 145/174] pip install dev --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3fa7df95..ea36ead2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ cache: pip python: 3.5 install: - - python setup.py install + - pip install -r requirements-dev.txt script: - python -m pytest tests/ From 285fc2ff1dae702ee0967aad8d52dbadb5689040 Mon Sep 17 00:00:00 2001 From: alimcmaster1 Date: Tue, 28 Aug 2018 22:25:34 +0100 Subject: [PATCH 146/174] Fix param name --- requirements-dev.txt | 1 - tests/test_authenticated_client.py | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c01f220d..6ff2e3ec 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,5 +4,4 @@ six==1.10.0 websocket-client==0.40.0 pymongo==3.5.1 pytest>=3.3.0 -pytest>=3.3.0 python-dateutil>=2.7.3 \ No newline at end of file diff --git a/tests/test_authenticated_client.py b/tests/test_authenticated_client.py index ebe10497..c680b3c7 100644 --- a/tests/test_authenticated_client.py +++ b/tests/test_authenticated_client.py @@ -21,13 +21,12 @@ def test_place_order_input_1(self, dc): def test_place_order_input_2(self, dc): with pytest.raises(ValueError): r = dc.place_order('BTC-USD', 'buy', 'limit', - cancel_after='123', tif='ABC') + cancel_after='123', time_in_force='ABC') - @pytest.mark.skip("Needs fixing") def test_place_order_input_3(self, dc): with pytest.raises(ValueError): r = dc.place_order('BTC-USD', 'buy', 'limit', - post_only='true', tif='FOK') + post_only='true', time_in_force='FOK') def test_place_order_input_4(self, dc): with pytest.raises(ValueError): From e5079c3ae802a59150afe58b42006fa7ac5c45e2 Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Tue, 28 Aug 2018 21:25:38 -0400 Subject: [PATCH 147/174] added travis icon to readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index cb24f291..611fee17 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![Build Status](https://travis-ci.org/danpaquin/coinbasepro-python.svg?branch=master)](https://travis-ci.org/danpaquin/coinbasepro-python) + # coinbasepro-python The Python client for the [Coinbase Pro API](https://docs.pro.coinbase.com/) (formerly known as the GDAX) From 3ec2c8cefb64481fb3cb56ca4a15bfc80a4f5f50 Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Tue, 28 Aug 2018 21:27:31 -0400 Subject: [PATCH 148/174] updated travis icon on readme --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 611fee17..e7e5236c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ -[![Build Status](https://travis-ci.org/danpaquin/coinbasepro-python.svg?branch=master)](https://travis-ci.org/danpaquin/coinbasepro-python) - # coinbasepro-python +[![Build Status](https://travis-ci.org/danpaquin/coinbasepro-python.svg?branch=master)](https://travis-ci.org/danpaquin/coinbasepro-python) The Python client for the [Coinbase Pro API](https://docs.pro.coinbase.com/) (formerly known as the GDAX) From 5af38452b1b0b9096c5c708252485e9bcbac714f Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Tue, 28 Aug 2018 21:28:43 -0400 Subject: [PATCH 149/174] updated travis icon on readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e7e5236c..75316412 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # coinbasepro-python [![Build Status](https://travis-ci.org/danpaquin/coinbasepro-python.svg?branch=master)](https://travis-ci.org/danpaquin/coinbasepro-python) + The Python client for the [Coinbase Pro API](https://docs.pro.coinbase.com/) (formerly known as the GDAX) From a3039cb9d88648fef3369940769d06870817e31e Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Wed, 5 Sep 2018 19:45:28 -0400 Subject: [PATCH 150/174] pip v1.1.3 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index da1dcdc0..198c909a 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ setup( name='cbpro', - version='1.1.2', + version='1.1.3', author='Daniel Paquin', author_email='dpaq34@gmail.com', license='MIT', From bd4e2eb1fb4a35c056d34ef5534fb4718dff7f45 Mon Sep 17 00:00:00 2001 From: Daniel Paquin Date: Wed, 5 Sep 2018 20:02:19 -0400 Subject: [PATCH 151/174] updated coinbase-withdraw endpoint --- cbpro/authenticated_client.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cbpro/authenticated_client.py b/cbpro/authenticated_client.py index f8a9152f..0c2dc329 100644 --- a/cbpro/authenticated_client.py +++ b/cbpro/authenticated_client.py @@ -867,7 +867,7 @@ def coinbase_withdraw(self, amount, currency, coinbase_account_id): params = {'amount': amount, 'currency': currency, 'coinbase_account_id': coinbase_account_id} - return self._send_message('post', '/withdrawals/coinbase', + return self._send_message('post', '/withdrawals/coinbase-account', data=json.dumps(params)) def crypto_withdraw(self, amount, currency, crypto_address): diff --git a/setup.py b/setup.py index 198c909a..38e457c1 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ setup( name='cbpro', - version='1.1.3', + version='1.1.4', author='Daniel Paquin', author_email='dpaq34@gmail.com', license='MIT', From 3923b528c6148094528d454cf032cd6b637e5e75 Mon Sep 17 00:00:00 2001 From: "Thomas Chen, ASA" Date: Tue, 30 Oct 2018 16:19:51 +0700 Subject: [PATCH 152/174] Fixes websocket dropped connections. This addresses #256. --- cbpro/websocket_client.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/cbpro/websocket_client.py b/cbpro/websocket_client.py index b598019f..f1b0f974 100644 --- a/cbpro/websocket_client.py +++ b/cbpro/websocket_client.py @@ -44,6 +44,7 @@ def _go(): self.stop = False self.on_open() self.thread = Thread(target=_go) + self.keepalive = Thread(target=self._keepalive) self.thread.start() def _connect(self): @@ -73,14 +74,15 @@ def _connect(self): self.ws.send(json.dumps(sub_params)) + def _keepalive(self, interval=30): + while self.ws.connected: + self.ws.ping("keepalive") + time.sleep(interval) + def _listen(self): + self.keepalive.start() while not self.stop: try: - start_t = 0 - if time.time() - start_t >= 30: - # Set a 30 second ping to keep connection alive - self.ws.ping("keepalive") - start_t = time.time() data = self.ws.recv() msg = json.loads(data) except ValueError as e: @@ -96,11 +98,14 @@ def _disconnect(self): self.ws.close() except WebSocketConnectionClosedException as e: pass + finally: + self.keepalive.join() self.on_close() def close(self): - self.stop = True + self.stop = True # will only disconnect after next msg recv + self._disconnect() # force disconnect so threads can join self.thread.join() def on_open(self): From e4ba196c3416e062ff5497a7d6e148ef874c422f Mon Sep 17 00:00:00 2001 From: Brenden Matthews Date: Mon, 26 Nov 2018 12:26:56 -0500 Subject: [PATCH 153/174] Update and fix dependencies. The way the dependencies are specified is too specific, and is causing a bunch of conflicts elsewhere. Let's update setup.py to do things the right way, then update requirements-dev.txt with `pip freeze > requirements-dev.txt`. --- .travis.yml | 2 +- requirements-dev.txt | 24 +++++++++++++++++------- setup.py | 12 ++++++++---- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index ea36ead2..dec568c6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ cache: pip python: 3.5 install: - - pip install -r requirements-dev.txt + - pip install .[test] script: - python -m pytest tests/ diff --git a/requirements-dev.txt b/requirements-dev.txt index 6ff2e3ec..7dfaedef 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,17 @@ -sortedcontainers>=1.5.9 -requests==2.13.0 -six==1.10.0 -websocket-client==0.40.0 -pymongo==3.5.1 -pytest>=3.3.0 -python-dateutil>=2.7.3 \ No newline at end of file +atomicwrites==1.2.1 +attrs==18.2.0 +cbpro==1.1.4 +certifi==2018.10.15 +chardet==3.0.4 +idna==2.7 +more-itertools==4.3.0 +pluggy==0.8.0 +py==1.7.0 +pymongo==3.7.2 +pytest==4.0.1 +python-dateutil==2.7.5 +requests==2.20.1 +six==1.11.0 +sortedcontainers==2.1.0 +urllib3==1.24.1 +websocket-client==0.54.0 diff --git a/setup.py b/setup.py index 38e457c1..ea047851 100644 --- a/setup.py +++ b/setup.py @@ -4,14 +4,15 @@ install_requires = [ 'sortedcontainers>=1.5.9', - 'requests==2.13.0', - 'six==1.10.0', - 'websocket-client==0.40.0', - 'pymongo==3.5.1' + 'requests>=2.13.0', + 'six>=1.10.0', + 'websocket-client>=0.40.0', + 'pymongo>=3.5.1', ] tests_require = [ 'pytest', + 'python-dateutil>=2.7.5', ] with open("README.md", "r") as fh: @@ -27,6 +28,9 @@ packages=find_packages(), install_requires=install_requires, tests_require=tests_require, + extras_require={ + 'test': tests_require, + }, description='The unofficial Python client for the Coinbase Pro API', long_description=long_description, long_description_content_type="text/markdown", From 646e99bbb1e883e47cbbbb8d1d8096a8f467c1aa Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 20 Sep 2019 13:26:42 -0400 Subject: [PATCH 154/174] Fix OrderBook class and require channels argument for WebsocketClient (#381) * Fix broken OrderBook by automatically connecting to 'full' channel Resolves an issue where the OrderBook class does not receive data since the underlying WebsocketClient it uses does not connect to a channel on Coinbase's WS feed. The OrderBook class now automatically specifies the 'full' channel on init in the super() call to the WebsocketClient __init__ * Require WS channels to be specified for WebsocketClient Coinbase's API rejects connections that don't specify which channels to connect to. This commit changes the `channels` arg for the WebsocketClient class into a required keyword argument with no default value. See: https://www.python.org/dev/peps/pep-3102/ Closes #380 Closes #371 --- cbpro/order_book.py | 3 ++- cbpro/websocket_client.py | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/cbpro/order_book.py b/cbpro/order_book.py index 790b7fce..0f393d9b 100644 --- a/cbpro/order_book.py +++ b/cbpro/order_book.py @@ -14,7 +14,8 @@ class OrderBook(WebsocketClient): def __init__(self, product_id='BTC-USD', log_to=None): - super(OrderBook, self).__init__(products=product_id) + super(OrderBook, self).__init__( + products=product_id, channels=['full']) self._asks = SortedDict() self._bids = SortedDict() self._client = PublicClient() diff --git a/cbpro/websocket_client.py b/cbpro/websocket_client.py index f1b0f974..dd7d5c22 100644 --- a/cbpro/websocket_client.py +++ b/cbpro/websocket_client.py @@ -18,8 +18,21 @@ class WebsocketClient(object): - def __init__(self, url="wss://ws-feed.pro.coinbase.com", products=None, message_type="subscribe", mongo_collection=None, - should_print=True, auth=False, api_key="", api_secret="", api_passphrase="", channels=None): + def __init__( + self, + url="wss://ws-feed.pro.coinbase.com", + products=None, + message_type="subscribe", + mongo_collection=None, + should_print=True, + auth=False, + api_key="", + api_secret="", + api_passphrase="", + # Make channels a required keyword-only argument; see pep3102 + *, + # Channel options: ['ticker', 'user', 'matches', 'level2', 'full'] + channels): self.url = url self.products = products self.channels = channels From 9355a9c95ec064989a436e4b063559bc93bd6491 Mon Sep 17 00:00:00 2001 From: Daniel Paquin <13591584+danpaquin@users.noreply.github.com> Date: Fri, 20 Sep 2019 13:29:06 -0400 Subject: [PATCH 155/174] Upgrade urllib3 to fix security vulnerability urllib3==1.24.2 --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 7dfaedef..1d286529 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,5 +13,5 @@ python-dateutil==2.7.5 requests==2.20.1 six==1.11.0 sortedcontainers==2.1.0 -urllib3==1.24.1 +urllib3==1.24.2 websocket-client==0.54.0 From b6f3a85301d9cf364f53cf80a5f8f288ef73f701 Mon Sep 17 00:00:00 2001 From: Matthew Ingersoll Date: Sun, 5 Apr 2020 07:29:33 -0700 Subject: [PATCH 156/174] Add authenticated client get_fees method and test (#384) * Add authenticated client get_fees method This adds the ability to get your: * current maker & taker fee rates * 30-day trailing volume See: https://docs.pro.coinbase.com/#fees * Add authenticated client get_fees test --- cbpro/authenticated_client.py | 13 +++++++++++++ tests/test_authenticated_client.py | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/cbpro/authenticated_client.py b/cbpro/authenticated_client.py index 0c2dc329..3013d423 100644 --- a/cbpro/authenticated_client.py +++ b/cbpro/authenticated_client.py @@ -995,3 +995,16 @@ def get_trailing_volume(self): """ return self._send_message('get', '/users/self/trailing-volume') + + def get_fees(self): + """ Get your maker & taker fee rates and 30-day trailing volume. + + Returns: + dict: Fee information and USD volume:: + { + "maker_fee_rate": "0.0015", + "taker_fee_rate": "0.0025", + "usd_volume": "25000.00" + } + """ + return self._send_message('get', '/fees') diff --git a/tests/test_authenticated_client.py b/tests/test_authenticated_client.py index c680b3c7..1643cc6e 100644 --- a/tests/test_authenticated_client.py +++ b/tests/test_authenticated_client.py @@ -186,3 +186,7 @@ def test_get_coinbase_accounts(self, client): def test_get_trailing_volume(self, client): r = client.get_trailing_volume() assert type(r) is list + + def test_get_fees(self, client): + r = client.get_fees() + assert type(r) is dict From 19cac25c51e592994cfa86ec487eef6a5c76718f Mon Sep 17 00:00:00 2001 From: adrielvieira Date: Sun, 5 Apr 2020 11:38:45 -0300 Subject: [PATCH 157/174] creates channels attributes based on products attribute in case channels are not provided in order to start connection (#393) Co-authored-by: Adriel Vieira --- cbpro/websocket_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cbpro/websocket_client.py b/cbpro/websocket_client.py index dd7d5c22..b0ffeb7d 100644 --- a/cbpro/websocket_client.py +++ b/cbpro/websocket_client.py @@ -70,7 +70,8 @@ def _connect(self): self.url = self.url[:-1] if self.channels is None: - sub_params = {'type': 'subscribe', 'product_ids': self.products} + self.channels = [{"name": "ticker", "product_ids": [product_id for product_id in self.products]}] + sub_params = {'type': 'subscribe', 'product_ids': self.products, 'channels': self.channels} else: sub_params = {'type': 'subscribe', 'product_ids': self.products, 'channels': self.channels} From 316d27fc488ba755aef2974f2a9439d19b705c0b Mon Sep 17 00:00:00 2001 From: Martin Michlmayr Date: Tue, 5 May 2020 16:08:27 +0800 Subject: [PATCH 158/174] Fix typo in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 75316412..f5cc6997 100644 --- a/README.md +++ b/README.md @@ -260,7 +260,7 @@ If you would like to receive real-time market updates, you must subscribe to the #### Subscribe to a single product ```python import cbpro -# Paramters are optional +# Parameters are optional wsClient = cbpro.WebsocketClient(url="wss://ws-feed.pro.coinbase.com", products="BTC-USD") # Do other stuff... wsClient.close() @@ -269,7 +269,7 @@ wsClient.close() #### Subscribe to multiple products ```python import cbpro -# Paramaters are optional +# Parameters are optional wsClient = cbpro.WebsocketClient(url="wss://ws-feed.pro.coinbase.com", products=["BTC-USD", "ETH-USD"]) # Do other stuff... From 1d2f993c8fb26e109579ded96db980741a545fd4 Mon Sep 17 00:00:00 2001 From: Jim Hribar Date: Sun, 19 Jan 2020 10:31:38 -0500 Subject: [PATCH 159/174] Corrections to README.md --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 75316412..9de7bd5b 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ public_client.get_time() Not all API endpoints are available to everyone. Those requiring user authentication can be reached using `AuthenticatedClient`. You must setup API access within your -[account settings](https://www.pro.coinbase.com/settings/api). +[account settings](https://pro.coinbase.com/settings/api). The `AuthenticatedClient` inherits all methods from the `PublicClient` class, so you will only need to initialize one if you are planning to integrate both into your script. @@ -261,7 +261,9 @@ If you would like to receive real-time market updates, you must subscribe to the ```python import cbpro # Paramters are optional -wsClient = cbpro.WebsocketClient(url="wss://ws-feed.pro.coinbase.com", products="BTC-USD") +wsClient = cbpro.WebsocketClient(url="wss://ws-feed.pro.coinbase.com", + products="BTC-USD", + channels=["ticker"]) # Do other stuff... wsClient.close() ``` @@ -271,7 +273,8 @@ wsClient.close() import cbpro # Paramaters are optional wsClient = cbpro.WebsocketClient(url="wss://ws-feed.pro.coinbase.com", - products=["BTC-USD", "ETH-USD"]) + products=["BTC-USD", "ETH-USD"], + channels=["ticker"]) # Do other stuff... wsClient.close() ``` From fa83a31bf1c99e722aad29ea6b06837472508b3f Mon Sep 17 00:00:00 2001 From: Jim Hribar Date: Sun, 25 Oct 2020 06:42:56 -0400 Subject: [PATCH 160/174] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9de7bd5b..2baac448 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ public_client.get_time() Not all API endpoints are available to everyone. Those requiring user authentication can be reached using `AuthenticatedClient`. You must setup API access within your -[account settings](https://pro.coinbase.com/settings/api). +[account settings](https://pro.coinbase.com/profile/api). The `AuthenticatedClient` inherits all methods from the `PublicClient` class, so you will only need to initialize one if you are planning to integrate both into your script. @@ -271,7 +271,7 @@ wsClient.close() #### Subscribe to multiple products ```python import cbpro -# Paramaters are optional +# Parameters are optional wsClient = cbpro.WebsocketClient(url="wss://ws-feed.pro.coinbase.com", products=["BTC-USD", "ETH-USD"], channels=["ticker"]) From 73b2de40684f22f3cdb700168da0977287066b94 Mon Sep 17 00:00:00 2001 From: JonasSteur Date: Sun, 28 Jul 2019 03:49:54 +0200 Subject: [PATCH 161/174] Fix creation of stop orders A 'stop' isn't an actual order type but is actually a special flavour of a limit order. The 'stop' and 'stop_price' params need to be set. There are 2 stop types: 1. loss (triggers at or below the stop price) -> this has to be a sell order to be valid (the Coinbase Pro will respond with an error otherwise) 2. entry (trigger at or above the stop price) -> has to be a buy Modified existing and added new test. --- cbpro/authenticated_client.py | 19 ++++++++++++++----- tests/test_authenticated_client.py | 14 +++++++++++--- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/cbpro/authenticated_client.py b/cbpro/authenticated_client.py index 3013d423..2d83d3b2 100644 --- a/cbpro/authenticated_client.py +++ b/cbpro/authenticated_client.py @@ -173,7 +173,7 @@ def get_account_holds(self, account_id, **kwargs): endpoint = '/accounts/{}/holds'.format(account_id) return self._send_paginated_message(endpoint, params=kwargs) - def place_order(self, product_id, side, order_type, **kwargs): + def place_order(self, product_id, side, order_type=None, **kwargs): """ Place an order. The three order types (limit, market, and stop) can be placed using this @@ -183,7 +183,7 @@ def place_order(self, product_id, side, order_type, **kwargs): Args: product_id (str): Product to order (eg. 'BTC-USD') side (str): Order side ('buy' or 'sell) - order_type (str): Order type ('limit', 'market', or 'stop') + order_type (str): Order type ('limit', or 'market') **client_oid (str): Order ID selected by you to identify your order. This should be a UUID, which will be broadcast in the public feed for `received` messages. @@ -243,7 +243,7 @@ def place_order(self, product_id, side, order_type, **kwargs): '`IOC` or `FOK`') # Market and stop order checks - if order_type == 'market' or order_type == 'stop': + if order_type == 'market' or kwargs.get('stop'): if not (kwargs.get('size') is None) ^ (kwargs.get('funds') is None): raise ValueError('Either `size` or `funds` must be specified ' 'for market/stop orders (but not both).') @@ -392,7 +392,7 @@ def place_market_order(self, product_id, side, size=None, funds=None, return self.place_order(**params) - def place_stop_order(self, product_id, side, price, size=None, funds=None, + def place_stop_order(self, product_id, side, stop_type, price, size=None, funds=None, client_oid=None, stp=None, overdraft_enabled=None, @@ -402,6 +402,9 @@ def place_stop_order(self, product_id, side, price, size=None, funds=None, Args: product_id (str): Product to order (eg. 'BTC-USD') side (str): Order side ('buy' or 'sell) + stop_type(str): Stop type ('entry' or 'loss') + loss: Triggers when the last trade price changes to a value at or below the stop_price. + entry: Triggers when the last trade price changes to a value at or above the stop_price price (Decimal): Desired price at which the stop order triggers. size (Optional[Decimal]): Desired amount in crypto. Specify this or `funds`. @@ -421,10 +424,16 @@ def place_stop_order(self, product_id, side, price, size=None, funds=None, dict: Order details. See `place_order` for example. """ + + if (side == 'buy' and stop_type == 'loss') or (side == 'sell' and stop_type == 'entry'): + raise ValueError(f'Invalid stop order, combination of {side} and {stop_type} is not possible') + params = {'product_id': product_id, 'side': side, 'price': price, - 'order_type': 'stop', + 'order_type': None, + 'stop': stop_type, + 'stop_price': price, 'size': size, 'funds': funds, 'client_oid': client_oid, diff --git a/tests/test_authenticated_client.py b/tests/test_authenticated_client.py index 1643cc6e..98229191 100644 --- a/tests/test_authenticated_client.py +++ b/tests/test_authenticated_client.py @@ -130,13 +130,21 @@ def test_place_market_order(self, client): r = client.place_market_order('BTC-USD', 'buy', funds=100000) assert type(r) is dict - def test_place_stop_order(self, client): + @pytest.mark.parametrize('stop_type, side', [('entry', 'buy'), ('loss', 'sell')]) + def test_place_stop_order(self, client, stop_type, side): client.cancel_all() - r = client.place_stop_order('BTC-USD', 'buy', 1, 0.01) + r = client.place_stop_order('BTC-USD', side, stop_type, 100, 0.01) assert type(r) is dict - assert r['type'] == 'stop' + assert r['stop'] == stop_type + assert r['stop_price'] == '100.00000000' + assert r['type'] == 'limit' client.cancel_order(r['id']) + def test_place_invalid_stop_order(self, client): + client.cancel_all() + with pytest.raises(ValueError): + client.place_stop_order('BTC-USD', 'buy', 'loss', 5.65, 0.01) + def test_cancel_order(self, client): r = client.place_limit_order('BTC-USD', 'buy', 4.43, 0.01232) time.sleep(0.2) From 1248de6d5ea23bb9bc1b75414fa19e758802e573 Mon Sep 17 00:00:00 2001 From: Mike Cardillo Date: Sat, 21 Nov 2020 15:52:19 -0800 Subject: [PATCH 162/174] Fix decimal places returned by server in test_place_stop_orer --- tests/test_authenticated_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_authenticated_client.py b/tests/test_authenticated_client.py index 98229191..7bd19bcf 100644 --- a/tests/test_authenticated_client.py +++ b/tests/test_authenticated_client.py @@ -136,7 +136,7 @@ def test_place_stop_order(self, client, stop_type, side): r = client.place_stop_order('BTC-USD', side, stop_type, 100, 0.01) assert type(r) is dict assert r['stop'] == stop_type - assert r['stop_price'] == '100.00000000' + assert r['stop_price'] == '100' assert r['type'] == 'limit' client.cancel_order(r['id']) From 73625bb3c650886a9b96796e126996bf6a9e1609 Mon Sep 17 00:00:00 2001 From: Mike Cardillo Date: Sat, 21 Nov 2020 16:13:12 -0800 Subject: [PATCH 163/174] Set 'side' directly in place_stop_order, instead of testing for valid side/stop_type pairs --- cbpro/authenticated_client.py | 11 +++++++---- tests/test_authenticated_client.py | 8 ++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/cbpro/authenticated_client.py b/cbpro/authenticated_client.py index 2d83d3b2..107afc4a 100644 --- a/cbpro/authenticated_client.py +++ b/cbpro/authenticated_client.py @@ -392,7 +392,7 @@ def place_market_order(self, product_id, side, size=None, funds=None, return self.place_order(**params) - def place_stop_order(self, product_id, side, stop_type, price, size=None, funds=None, + def place_stop_order(self, product_id, stop_type, price, size=None, funds=None, client_oid=None, stp=None, overdraft_enabled=None, @@ -401,7 +401,6 @@ def place_stop_order(self, product_id, side, stop_type, price, size=None, funds= Args: product_id (str): Product to order (eg. 'BTC-USD') - side (str): Order side ('buy' or 'sell) stop_type(str): Stop type ('entry' or 'loss') loss: Triggers when the last trade price changes to a value at or below the stop_price. entry: Triggers when the last trade price changes to a value at or above the stop_price @@ -425,8 +424,12 @@ def place_stop_order(self, product_id, side, stop_type, price, size=None, funds= """ - if (side == 'buy' and stop_type == 'loss') or (side == 'sell' and stop_type == 'entry'): - raise ValueError(f'Invalid stop order, combination of {side} and {stop_type} is not possible') + if stop_type == 'loss': + side = 'sell' + elif stop_type == 'entry': + side = 'buy' + else: + raise ValueError(f'Invalid stop_type for stop order: {stop_type}') params = {'product_id': product_id, 'side': side, diff --git a/tests/test_authenticated_client.py b/tests/test_authenticated_client.py index 7bd19bcf..40edfac8 100644 --- a/tests/test_authenticated_client.py +++ b/tests/test_authenticated_client.py @@ -130,10 +130,10 @@ def test_place_market_order(self, client): r = client.place_market_order('BTC-USD', 'buy', funds=100000) assert type(r) is dict - @pytest.mark.parametrize('stop_type, side', [('entry', 'buy'), ('loss', 'sell')]) - def test_place_stop_order(self, client, stop_type, side): + @pytest.mark.parametrize('stop_type', ['entry', 'loss']) + def test_place_stop_order(self, client, stop_type): client.cancel_all() - r = client.place_stop_order('BTC-USD', side, stop_type, 100, 0.01) + r = client.place_stop_order('BTC-USD', stop_type, 100, 0.01) assert type(r) is dict assert r['stop'] == stop_type assert r['stop_price'] == '100' @@ -143,7 +143,7 @@ def test_place_stop_order(self, client, stop_type, side): def test_place_invalid_stop_order(self, client): client.cancel_all() with pytest.raises(ValueError): - client.place_stop_order('BTC-USD', 'buy', 'loss', 5.65, 0.01) + client.place_stop_order('BTC-USD', 'fake_stop_type', 5.65, 0.01) def test_cancel_order(self, client): r = client.place_limit_order('BTC-USD', 'buy', 4.43, 0.01232) From 8186cf2b5e55a3f48dece70dae367576e86824e4 Mon Sep 17 00:00:00 2001 From: Mike Cardillo Date: Sat, 21 Nov 2020 16:16:59 -0800 Subject: [PATCH 164/174] Updated new place_stop_order example in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b5251088..37872be3 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,7 @@ auth_client.place_market_order(product_id='BTC-USD', ```python # Stop order. `funds` can be used instead of `size` here. auth_client.place_stop_order(product_id='BTC-USD', - side='buy', + stop_type='loss', price='200.00', size='0.01') ``` From 559dee43b5975f7cd9969d252e3c995ef3ebc49d Mon Sep 17 00:00:00 2001 From: freenancial Date: Wed, 1 Jul 2020 22:47:29 -0700 Subject: [PATCH 165/174] Add stablecoin conversion --- cbpro/authenticated_client.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/cbpro/authenticated_client.py b/cbpro/authenticated_client.py index 107afc4a..d92279c4 100644 --- a/cbpro/authenticated_client.py +++ b/cbpro/authenticated_client.py @@ -173,6 +173,33 @@ def get_account_holds(self, account_id, **kwargs): endpoint = '/accounts/{}/holds'.format(account_id) return self._send_paginated_message(endpoint, params=kwargs) + + def convert_stablecoin(self, amount, from_currency, to_currency): + """ Convert stablecoin. + + Args: + amount (Decimal): The amount to convert. + from_currency (str): Currency type (eg. 'USDC') + to_currency (str): Currency type (eg. 'USD'). + + Returns: + dict: Conversion details. Example:: + { + "id": "8942caee-f9d5-4600-a894-4811268545db", + "amount": "10000.00", + "from_account_id": "7849cc79-8b01-4793-9345-bc6b5f08acce", + "to_account_id": "105c3e58-0898-4106-8283-dc5781cda07b", + "from": "USD", + "to": "USDC" + } + + """ + params = {'from': from_currency, + 'to': to_currency, + 'amount': amount} + return self._send_message('post', '/conversions', data=json.dumps(params)) + + def place_order(self, product_id, side, order_type=None, **kwargs): """ Place an order. From 45ed5dcc1c1862f2a89c74ee3092352527144098 Mon Sep 17 00:00:00 2001 From: Mike Cardillo Date: Sat, 21 Nov 2020 17:35:35 -0800 Subject: [PATCH 166/174] Added test_convert_stablecoin --- tests/test_authenticated_client.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_authenticated_client.py b/tests/test_authenticated_client.py index 40edfac8..36ae1beb 100644 --- a/tests/test_authenticated_client.py +++ b/tests/test_authenticated_client.py @@ -107,6 +107,14 @@ def test_get_account_holds(self, client): assert 'type' in r[0] assert 'ref' in r[0] + def test_convert_stablecoin(self, client): + r = client.convert_stablecoin('10.0', 'USD', 'USDC') + assert type(r) is dict + assert 'id' in r + assert r['amount'] == '10.00000000' + assert r['from'] == 'USD' + assert r['to'] == 'USDC' + def test_place_order(self, client): r = client.place_order('BTC-USD', 'buy', 'limit', price=0.62, size=0.0144) From 5658b2212b0fe39dde18b792f34aeaf81dda6640 Mon Sep 17 00:00:00 2001 From: Mike Cardillo Date: Sat, 21 Nov 2020 18:20:51 -0800 Subject: [PATCH 167/174] Remove fstring from place_stop_order to make compatible with python < 3.6 --- cbpro/authenticated_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cbpro/authenticated_client.py b/cbpro/authenticated_client.py index d92279c4..f330377f 100644 --- a/cbpro/authenticated_client.py +++ b/cbpro/authenticated_client.py @@ -456,7 +456,7 @@ def place_stop_order(self, product_id, stop_type, price, size=None, funds=None, elif stop_type == 'entry': side = 'buy' else: - raise ValueError(f'Invalid stop_type for stop order: {stop_type}') + raise ValueError('Invalid stop_type for stop order: ' + stop_type) params = {'product_id': product_id, 'side': side, From 4e8b04989b230a9fa722e4dffc66492048930230 Mon Sep 17 00:00:00 2001 From: Vel Lesikov Date: Thu, 24 Jun 2021 18:30:12 -0700 Subject: [PATCH 168/174] Update requests version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ea047851..47e1ca9c 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ install_requires = [ 'sortedcontainers>=1.5.9', - 'requests>=2.13.0', + 'requests>=2.25.0', 'six>=1.10.0', 'websocket-client>=0.40.0', 'pymongo>=3.5.1', From 5d38d21f523885340cda8b8b3fbfb091ca49a099 Mon Sep 17 00:00:00 2001 From: Mike Cardillo Date: Mon, 27 Nov 2017 16:05:13 -0800 Subject: [PATCH 169/174] Removed WebsocketClient inheritance from OrderBook class This is meant to change the application workflow to only require one WebsocketClient, which can feed messages to data structures for further processing. --- cbpro/order_book.py | 144 +++++++++++++++++++++++--------------------- 1 file changed, 76 insertions(+), 68 deletions(-) diff --git a/cbpro/order_book.py b/cbpro/order_book.py index 0f393d9b..beeefc74 100644 --- a/cbpro/order_book.py +++ b/cbpro/order_book.py @@ -6,16 +6,15 @@ from sortedcontainers import SortedDict from decimal import Decimal +import Queue import pickle from cbpro.public_client import PublicClient from cbpro.websocket_client import WebsocketClient -class OrderBook(WebsocketClient): +class OrderBook(object): def __init__(self, product_id='BTC-USD', log_to=None): - super(OrderBook, self).__init__( - products=product_id, channels=['full']) self._asks = SortedDict() self._bids = SortedDict() self._client = PublicClient() @@ -24,18 +23,7 @@ def __init__(self, product_id='BTC-USD', log_to=None): if self._log_to: assert hasattr(self._log_to, 'write') self._current_ticker = None - - @property - def product_id(self): - ''' Currently OrderBook only supports a single product even though it is stored as a list of products. ''' - return self.products[0] - - def on_open(self): - self._sequence = -1 - print("-- Subscribed to OrderBook! --\n") - - def on_close(self): - print("\n-- OrderBook Socket Closed! --") + self.product_id = product_id def reset_book(self): self._asks = SortedDict() @@ -57,33 +45,34 @@ def reset_book(self): }) self._sequence = res['sequence'] - def on_message(self, message): - if self._log_to: - pickle.dump(message, self._log_to) + def process_message(self, message): + if message.get('product_id') == self.product_id: + if self._log_to: + pickle.dump(message, self._log_to) - sequence = message.get('sequence', -1) - if self._sequence == -1: - self.reset_book() - return - if sequence <= self._sequence: - # ignore older messages (e.g. before order book initialization from getProductOrderBook) - return - elif sequence > self._sequence + 1: - self.on_sequence_gap(self._sequence, sequence) - return + sequence = message.get('sequence', -1) + if self._sequence == -1: + self.reset_book() + return + if sequence <= self._sequence: + # ignore older messages (e.g. before order book initialization from getProductOrderBook) + return + elif sequence > self._sequence + 1: + self.on_sequence_gap(self._sequence, sequence) + return - msg_type = message['type'] - if msg_type == 'open': - self.add(message) - elif msg_type == 'done' and 'price' in message: - self.remove(message) - elif msg_type == 'match': - self.match(message) - self._current_ticker = message - elif msg_type == 'change': - self.change(message) + msg_type = message['type'] + if msg_type == 'open': + self.add(message) + elif msg_type == 'done' and 'price' in message: + self.remove(message) + elif msg_type == 'match': + self.match(message) + self._current_ticker = message + elif msg_type == 'change': + self.change(message) - self._sequence = sequence + self._sequence = sequence def on_sequence_gap(self, gap_start, gap_end): self.reset_book() @@ -249,7 +238,6 @@ def set_bids(self, price, bids): import time import datetime as dt - class OrderBookConsole(OrderBook): ''' Logs real-time changes to the bid-ask spread to the console ''' @@ -262,38 +250,58 @@ def __init__(self, product_id=None): self._bid_depth = None self._ask_depth = None - def on_message(self, message): - super(OrderBookConsole, self).on_message(message) - - # Calculate newest bid-ask spread - bid = self.get_bid() - bids = self.get_bids(bid) - bid_depth = sum([b['size'] for b in bids]) - ask = self.get_ask() - asks = self.get_asks(ask) - ask_depth = sum([a['size'] for a in asks]) - - if self._bid == bid and self._ask == ask and self._bid_depth == bid_depth and self._ask_depth == ask_depth: - # If there are no changes to the bid-ask spread since the last update, no need to print - pass - else: - # If there are differences, update the cache - self._bid = bid - self._ask = ask - self._bid_depth = bid_depth - self._ask_depth = ask_depth - print('{} {} bid: {:.3f} @ {:.2f}\task: {:.3f} @ {:.2f}'.format( - dt.datetime.now(), self.product_id, bid_depth, bid, ask_depth, ask)) - - order_book = OrderBookConsole() - order_book.start() + def process_message(self, message): + if message.get('product_id') == self.product_id: + super(OrderBookConsole, self).process_message(message) + + try: + # Calculate newest bid-ask spread + bid = self.get_bid() + bids = self.get_bids(bid) + bid_depth = sum([b['size'] for b in bids]) + ask = self.get_ask() + asks = self.get_asks(ask) + ask_depth = sum([a['size'] for a in asks]) + + if self._bid == bid and self._ask == ask and self._bid_depth == bid_depth and self._ask_depth == ask_depth: + # If there are no changes to the bid-ask spread since the last update, no need to print + pass + else: + # If there are differences, update the cache + self._bid = bid + self._ask = ask + self._bid_depth = bid_depth + self._ask_depth = ask_depth + print('{} {} bid: {:.3f} @ {:.2f}\task: {:.3f} @ {:.2f}'.format( + dt.datetime.now(), self.product_id, bid_depth, bid, ask_depth, ask)) + except Exception: + pass + + class WebsocketConsole(WebsocketClient): + def on_open(self): + self.products = ['BTC-USD', 'ETH-USD'] + self.websocket_queue = Queue.Queue() + + def on_message(self, msg): + self.websocket_queue.put(msg) + + order_book_btc = OrderBookConsole(product_id='BTC-USD') + order_book_eth = OrderBookConsole(product_id='ETH-USD') + + wsClient = WebsocketConsole() + wsClient.start() + time.sleep(10) try: while True: - time.sleep(10) + msg = wsClient.websocket_queue.get(timeout=15) + order_book_btc.process_message(msg) + order_book_eth.process_message(msg) except KeyboardInterrupt: - order_book.close() + wsClient.close() + except Exception: + pass - if order_book.error: + if wsClient.error: sys.exit(1) else: sys.exit(0) From bb66088ddeec2de63ac101d8a81c6c11a921da15 Mon Sep 17 00:00:00 2001 From: Mike Cardillo Date: Mon, 27 Nov 2017 17:07:36 -0800 Subject: [PATCH 170/174] Updated README to reflect OrderBook changes --- README.md | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 37872be3..b1a63c14 100644 --- a/README.md +++ b/README.md @@ -346,16 +346,30 @@ python -m pytest ``` ### Real-time OrderBook -The ```OrderBook``` subscribes to a websocket and keeps a real-time record of -the orderbook for the product_id input. Please provide your feedback for future +The ```OrderBook``` is a convenient data structure to keep a real-time record of +the orderbook for the product_id input. It processes incoming messages from an +already existing WebsocketClient. Please provide your feedback for future improvements. ```python -import cbpro, time -order_book = cbpro.OrderBook(product_id='BTC-USD') -order_book.start() +import cbpro, time, Queue +class myWebsocketClient(cbpro.WebsocketClient): + def on_open(self): + self.products = ['BTC-USD', 'ETH-USD'] + self.websocket_queue = Queue.Queue() + def on_message(self, msg): + self.websocket_queue.put(msg) + +order_book_btc = cbpro.OrderBook(product_id='BTC-USD') +order_book_eth = cbpro.OrderBook(product_id='ETH-USD') +wsClient = myWebsocketClient() +wsClient.start() time.sleep(10) -order_book.close() +while True: + msg = wsClient.websocket_queue.get(timeout=15) + order_book.process_message(msg) + print(order_book_btc.get_ask()) + print(order_book_eth.get_bid()) ``` ### Testing From 002fdfe4ca5b6b124088fe124f6687abdd2cbae7 Mon Sep 17 00:00:00 2001 From: Mike Cardillo Date: Thu, 30 Nov 2017 13:50:13 -0800 Subject: [PATCH 171/174] Remove print from OrderBook.on_sequence_gap() --- cbpro/order_book.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/cbpro/order_book.py b/cbpro/order_book.py index beeefc74..035fa52f 100644 --- a/cbpro/order_book.py +++ b/cbpro/order_book.py @@ -76,9 +76,6 @@ def process_message(self, message): def on_sequence_gap(self, gap_start, gap_end): self.reset_book() - print('Error: messages missing ({} - {}). Re-initializing book at sequence.'.format( - gap_start, gap_end, self._sequence)) - def add(self, order): order = { From 53ab2f3a9630bfea0b54d3810eba1b0c81507942 Mon Sep 17 00:00:00 2001 From: Mike Cardillo Date: Thu, 30 Nov 2017 13:57:21 -0800 Subject: [PATCH 172/174] Updated contributors --- contributors.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contributors.txt b/contributors.txt index b1e5c495..3a6695b6 100644 --- a/contributors.txt +++ b/contributors.txt @@ -3,4 +3,5 @@ Leonard Lin Jeff Gibson David Caseria Paul Mestemaker -Drew Rice \ No newline at end of file +Drew Rice +Mike Cardillo \ No newline at end of file From 13147c0c923e3afe91f889ad3572391e7a158e71 Mon Sep 17 00:00:00 2001 From: Mike Cardillo Date: Sun, 10 Dec 2017 14:29:46 -0800 Subject: [PATCH 173/174] Removed reference to Queue in WebsocketConsole Removed to maintain backward compatability --- cbpro/order_book.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/cbpro/order_book.py b/cbpro/order_book.py index 035fa52f..1a8525e5 100644 --- a/cbpro/order_book.py +++ b/cbpro/order_book.py @@ -6,7 +6,6 @@ from sortedcontainers import SortedDict from decimal import Decimal -import Queue import pickle from cbpro.public_client import PublicClient @@ -277,22 +276,19 @@ def process_message(self, message): class WebsocketConsole(WebsocketClient): def on_open(self): self.products = ['BTC-USD', 'ETH-USD'] - self.websocket_queue = Queue.Queue() + self.order_book_btc = OrderBookConsole(product_id='BTC-USD') + self.order_book_eth = OrderBookConsole(product_id='ETH-USD') def on_message(self, msg): - self.websocket_queue.put(msg) - - order_book_btc = OrderBookConsole(product_id='BTC-USD') - order_book_eth = OrderBookConsole(product_id='ETH-USD') + self.order_book_btc.process_message(msg) + self.order_book_eth.process_message(msg) wsClient = WebsocketConsole() wsClient.start() time.sleep(10) try: while True: - msg = wsClient.websocket_queue.get(timeout=15) - order_book_btc.process_message(msg) - order_book_eth.process_message(msg) + pass except KeyboardInterrupt: wsClient.close() except Exception: From 789bbab7c5183d9d29f956b24a2ad5ae9705549c Mon Sep 17 00:00:00 2001 From: Mike Cardillo Date: Sun, 10 Dec 2017 21:27:46 -0800 Subject: [PATCH 174/174] Updated README to give example without Queue --- README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b1a63c14..456d212e 100644 --- a/README.md +++ b/README.md @@ -356,20 +356,19 @@ import cbpro, time, Queue class myWebsocketClient(cbpro.WebsocketClient): def on_open(self): self.products = ['BTC-USD', 'ETH-USD'] - self.websocket_queue = Queue.Queue() + self.order_book_btc = OrderBookConsole(product_id='BTC-USD') + self.order_book_eth = OrderBookConsole(product_id='ETH-USD') def on_message(self, msg): - self.websocket_queue.put(msg) + self.order_book_btc.process_message(msg) + self.order_book_eth.process_message(msg) -order_book_btc = cbpro.OrderBook(product_id='BTC-USD') -order_book_eth = cbpro.OrderBook(product_id='ETH-USD') wsClient = myWebsocketClient() wsClient.start() time.sleep(10) while True: - msg = wsClient.websocket_queue.get(timeout=15) - order_book.process_message(msg) - print(order_book_btc.get_ask()) - print(order_book_eth.get_bid()) + print(wsClient.order_book_btc.get_ask()) + print(wsClient.order_book_eth.get_bid()) + time.sleep(1) ``` ### Testing