From 79f99f25f9a3c40693a10947435928f19fd83dc8 Mon Sep 17 00:00:00 2001 From: acontry Date: Tue, 13 Jun 2017 22:28:56 -0700 Subject: [PATCH 01/72] 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 c36a7ef8c414af618f9b5492ab5cfb101b5de8cf Mon Sep 17 00:00:00 2001 From: acontry Date: Thu, 15 Jun 2017 00:37:59 -0700 Subject: [PATCH 02/72] 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 c3732a1ac2949436ccab49a46c68ca3513513e9b Mon Sep 17 00:00:00 2001 From: dj Date: Sun, 18 Jun 2017 23:31:13 -0400 Subject: [PATCH 03/72] 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 04/72] 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 373a690571f0e12e0010a91d1681b284cfdce502 Mon Sep 17 00:00:00 2001 From: DJ Strasser Date: Thu, 22 Jun 2017 15:59:14 -0400 Subject: [PATCH 05/72] 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 2327a1349cc948fcbd057f0eb6b245f45c077e56 Mon Sep 17 00:00:00 2001 From: acontry Date: Sat, 1 Jul 2017 23:44:13 -0700 Subject: [PATCH 06/72] 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 07/72] 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 5c9bc2b375a4687e3e6767cc9cb41ff0f94f4d62 Mon Sep 17 00:00:00 2001 From: acontry Date: Thu, 21 Sep 2017 23:32:37 -0700 Subject: [PATCH 08/72] 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 09/72] 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 aa0b9d7cdda4b67936da7689ed5df7589c92f22f Mon Sep 17 00:00:00 2001 From: Gabriel Smadi Date: Sat, 17 Feb 2018 22:23:04 -0500 Subject: [PATCH 10/72] 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 11/72] 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 12/72] 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 49613dec472d55a6a3e8bdd845de5d02ec2cb96d Mon Sep 17 00:00:00 2001 From: Aaron Adler Date: Mon, 18 Jun 2018 23:44:22 -0700 Subject: [PATCH 13/72] 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 14/72] 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 15/72] 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 16/72] 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 17/72] 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 18/72] 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 19/72] 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 20/72] 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 21/72] 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 22/72] 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 23/72] 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 24/72] 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 25/72] 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 26/72] 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 27/72] 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 28/72] 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 29/72] 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 30/72] 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 31/72] 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 32/72] 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 33/72] 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 34/72] 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 35/72] 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 36/72] 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 37/72] 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 38/72] 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 39/72] 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 40/72] 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 41/72] 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 42/72] 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 43/72] 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 44/72] 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 45/72] 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 46/72] 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 47/72] 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 48/72] 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 49/72] 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 50/72] 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 51/72] 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 52/72] 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 53/72] 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 54/72] 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 55/72] 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 56/72] 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 57/72] 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 58/72] 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 59/72] 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 60/72] 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 61/72] 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 62/72] 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 63/72] 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 64/72] 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 65/72] 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 66/72] 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 67/72] 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 68/72] 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 69/72] 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 70/72] 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 71/72] 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 72/72] 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