├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── gemini ├── __init__.py ├── client.py └── error.py ├── requirements.txt ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | test.py 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # IPython Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # dotenv 80 | .env 81 | 82 | # virtualenv 83 | venv3/ 84 | venv/ 85 | ENV/ 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.2" 4 | - "3.3" 5 | - "3.4" 6 | - "3.5" 7 | - "3.6" 8 | install: "pip install -r requirements.txt" 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Matt Selph 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gemini-python-unoffc 2 | Unofficial Python library for the [Gemini Exchange](https://gemini.com) REST API. This library has methods for all public, private, and order execution methods. The API documentation can be found [here](https://docs.gemini.com/rest-api/). 3 | 4 | Installation 5 | ------------ 6 | We're on PyPi! This library is compatible with Python 2 and 3. 7 | 8 | ``` 9 | pip install gemini-python-unoffc 10 | ``` 11 | 12 | Usage 13 | ----- 14 | You need to complete two steps before you can use the library: 15 | 16 | 1. Register for a [Gemini Exchange Account](https://exchange.gemini.com/register). They also offer a [Sandbox](https://exchange.sandbox.gemini.com/register) for testing purposes. 17 | 2. Once registered, obtain an API key and API secret. 18 | 19 | Now you are ready to use the library. Everything is called via a `Client` object: 20 | 21 | ```python 22 | from gemini.client import Client 23 | 24 | c = Client(api_key='API_KEY', api_secret='API_SECRET', sandbox=True) 25 | ``` 26 | 27 | Note the `sandbox` argument. The default is `False`, so if you want to test on the exchange's sandbox, you need to override this argument and excplicitly set it to `True`. 28 | 29 | The `Client` object returns JSON from the exchange's API. 30 | 31 | Example 32 | -------- 33 | ```python 34 | from gemini.client import Client 35 | 36 | c = Client('API_KEY','API_SECRET') 37 | print(c.get_symbols()) 38 | 39 | print(c.get_balance()) 40 | ``` 41 | 42 | Future Enhancements 43 | ------------------- 44 | Some feautres planned for later releases include: 45 | - 100% test coverage 46 | - Websocket interface 47 | - Travis CI builds (goes along with 100% test coverage) 48 | - Those neat lil' icons showing "Build Passing," and "100% test coverage" 49 | 50 | License 51 | ------- 52 | This software is licensed under the [MIT License](https://github.com/mattselph/gemini-python-unoffc/blob/master/LICENSE). 53 | -------------------------------------------------------------------------------- /gemini/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | __version__ = '0.1' 3 | -------------------------------------------------------------------------------- /gemini/client.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from gemini.error import raise_api_error 4 | 5 | import requests 6 | import base64 7 | import hashlib 8 | import hmac 9 | import json 10 | import time 11 | 12 | class Client(object): 13 | """ Client for the Gemini Exchange REST API. 14 | 15 | Full API docs are here: https://docs.gemini.com 16 | """ 17 | 18 | API_VERSION = '/v1' 19 | 20 | def __init__(self, api_key, api_secret, sandbox=False): 21 | self.API_KEY = api_key 22 | self.API_SECRET = api_secret 23 | 24 | if not sandbox: 25 | self.BASE_URI = "https://api.gemini.com" + self.API_VERSION 26 | else: 27 | self.BASE_URI = "https://api.sandbox.gemini.com" + self.API_VERSION 28 | 29 | # Private API methods 30 | # ------------------- 31 | def _get_nonce(self): 32 | return time.time()*1000 33 | 34 | def _handle_response(self, request, response): 35 | """ Handles all responses from the API. Checks the return HTTP status code and formats the response in JSON. """ 36 | status_code = str(response.status_code) 37 | 38 | if not status_code.startswith('2'): 39 | raise raise_api_error(request, response) 40 | else: 41 | return response.json() 42 | 43 | def _invoke_api(self, endpoint, payload, params=None, pub=True): 44 | """ Sends the request to the Gemini Exchange API. 45 | 46 | Args: 47 | endpoint (str): URL the call will go to 48 | payload (dict): Headers containing the request specifics 49 | params (dict, optional): A dict containing URL parameters (for public API calls) 50 | pub(bool, optional): Boolean value identifying a Public API call (True) or Private API call (False) 51 | """ 52 | 53 | # base64 encode the payload 54 | payload = str.encode(json.dumps(payload)) 55 | b64 = base64.b64encode(payload) 56 | 57 | # sign the requests 58 | signature = hmac.new(str.encode(self.API_SECRET), b64, hashlib.sha384).hexdigest() 59 | 60 | headers = { 61 | 'Content-Type': 'text/plain', 62 | 'X-GEMINI-APIKEY': self.API_KEY, 63 | 'X-GEMINI-PAYLOAD': b64, 64 | 'X-GEMINI-SIGNATURE': signature 65 | } 66 | 67 | url = self.BASE_URI + endpoint 68 | 69 | # build a request object in case there's an error so we can echo it 70 | request = {'payload': payload, 'headers': headers, 'url': url} 71 | 72 | if not pub: 73 | # private api methods are POSTs 74 | response = requests.post(url, headers=headers) 75 | else: 76 | response = requests.get(url, headers=headers, params=params) 77 | 78 | return self._handle_response(request, response) 79 | 80 | # Public API methods 81 | # ------------------ 82 | def get_symbols(self): 83 | """ https://docs.gemini.com/rest-api/#symbols """ 84 | endpoint = '/symbols' 85 | 86 | payload = { 87 | 'request': self.API_VERSION + endpoint, 88 | 'nonce': self._get_nonce() 89 | } 90 | 91 | return self._invoke_api(endpoint, payload, pub=True) 92 | 93 | def get_ticker(self, symbol): 94 | """ https://docs.gemini.com/rest-api/#ticker """ 95 | endpoint = '/pubticker/' + symbol 96 | 97 | payload = { 98 | 'request': self.API_VERSION + endpoint, 99 | 'nonce': self._get_nonce() 100 | } 101 | 102 | return self._invoke_api(endpoint, payload, pub=True) 103 | 104 | def get_order_book(self, symbol): 105 | """ https://docs.gemini.com/rest-api/#current-order-book """ 106 | endpoint = '/book/' + symbol 107 | 108 | payload = { 109 | 'request': self.API_VERSION + endpoint, 110 | 'nonce': self._get_nonce() 111 | } 112 | 113 | return self._invoke_api(endpoint, payload, pub=True) 114 | 115 | def get_trade_history(self, symbol, since=None, limit_trades=None, include_breaks=None): 116 | """ https://docs.gemini.com/rest-api/#trade-history """ 117 | 118 | # build URL parameters 119 | params = {} 120 | if since: 121 | params['since'] = since 122 | 123 | if limit_trades: 124 | params['limit_trades'] = limit_trades 125 | 126 | if include_breaks: 127 | params['include_breaks'] = include_breaks 128 | 129 | endpoint = '/trades/' + symbol 130 | 131 | payload = { 132 | 'request': self.API_VERSION + endpoint, 133 | 'nonce': self._get_nonce() 134 | } 135 | 136 | return self._invoke_api(endpoint, payload, params, pub=True) 137 | 138 | def get_current_auction(self, symbol): 139 | """ https://docs.gemini.com/rest-api/#current-auction """ 140 | endpoint = '/auction/' + symbol 141 | 142 | payload = { 143 | 'request': self.API_VERSION + endpoint, 144 | 'nonce': self._get_nonce() 145 | } 146 | 147 | return self._invoke_api(endpoint, payload, pub=True) 148 | 149 | def get_auction_history(self, symbol, since=None, limit_auction_results=None, include_indicative=None): 150 | """ https://docs.gemini.com/rest-api/#auction-history """ 151 | 152 | # build URL parameters 153 | params = {} 154 | if since: 155 | params['since'] = since 156 | 157 | if limit_auction_results: 158 | params['limit_auction_results'] = limit_auction_results 159 | 160 | if include_indicative: 161 | params['include_indicative'] = include_indicative 162 | 163 | endpoint = '/auction/' + symbol + '/history' 164 | 165 | payload = { 166 | 'request': self.API_VERSION + endpoint, 167 | 'nonce': self._get_nonce() 168 | } 169 | 170 | return self._invoke_api(endpoint, payload, params, pub=True) 171 | 172 | # Order Status API 173 | # https://docs.gemini.com/rest-api/#order-status 174 | # ---------------------------------------------- 175 | def get_active_orders(self): 176 | """ https://docs.gemini.com/rest-api/#get-active-orders """ 177 | endpoint = '/orders' 178 | 179 | payload = { 180 | 'request': self.API_VERSION + endpoint, 181 | 'nonce': self._get_nonce() 182 | } 183 | 184 | return self._invoke_api(endpoint, payload, pub=False) 185 | 186 | def get_order_status(self, order_id): 187 | """ https://docs.gemini.com/rest-api/#order-status """ 188 | endpoint = '/order/status' 189 | 190 | payload = { 191 | 'request': self.API_VERSION + endpoint, 192 | 'nonce': self._get_nonce(), 193 | 'order_id': order_id 194 | } 195 | 196 | return self._invoke_api(endpoint, payload, pub=False) 197 | 198 | def get_trade_volume(self): 199 | """ https://docs.gemini.com/rest-api/#get-trade-volume """ 200 | endpoint = '/tradevolume' 201 | 202 | payload = { 203 | 'request': self.API_VERSION + endpoint, 204 | 'nonce': self._get_nonce() 205 | } 206 | 207 | return self._invoke_api(endpoint, payload, pub=False) 208 | 209 | def get_past_trades(self, symbol, limit_trades, timestamp=None): 210 | """ https://docs.gemini.com/rest-api/#get-past-trades """ 211 | endpoint = '/mytrades' 212 | 213 | payload = { 214 | 'request': self.API_VERSION + endpoint, 215 | 'nonce': self._get_nonce(), 216 | 'symbol': symbol, 217 | 'limit_trades': limit_trades, 218 | 'timestamp': timestamp 219 | } 220 | 221 | return self._invoke_api(endpoint, payload, pub=False) 222 | 223 | # Order Placement API 224 | # https://docs.gemini.com/rest-api/#new-order 225 | # ------------------------------------------- 226 | def new_order(self, client_order_id, symbol, amount, price, side, type, options=None): 227 | """ https://docs.gemini.com/rest-api/#new-order """ 228 | endpoint = '/order/new' 229 | 230 | payload = { 231 | 'request': self.API_VERSION + endpoint, 232 | 'nonce': self._get_nonce(), 233 | 'client_order_id': client_order_id, 234 | 'symbol': symbol, 235 | 'amount': amount, 236 | 'price': price, 237 | 'side': side, 238 | 'type': 'exchange limit', 239 | 'options': options 240 | } 241 | 242 | return self._invoke_api(endpoint, payload, pub=False) 243 | 244 | def cancel_order(self, order_id): 245 | """ https://docs.gemini.com/rest-api/#cancel-order """ 246 | endpoint = '/order/cancel' 247 | 248 | payload = { 249 | 'request': self.API_VERSION + endpoint, 250 | 'nonce': self._get_nonce(), 251 | 'order_id': order_id 252 | } 253 | 254 | return self._invoke_api(endpoint, payload, pub=False) 255 | 256 | def cancel_session_orders(self): 257 | """ https://docs.gemini.com/rest-api/#cancel-all-session-orders """ 258 | endpoint = '/order/cancel/session' 259 | 260 | payload = { 261 | 'request': self.API_VERSION + endpoint, 262 | 'nonce': self._get_nonce() 263 | } 264 | 265 | return self._invoke_api(endpoint, payload, pub=False) 266 | 267 | def cancel_all_orders(self): 268 | """ https://docs.gemini.com/rest-api/#cancel-all-active-orders """ 269 | endpoint = '/order/cancel/all' 270 | 271 | payload = { 272 | 'request': self.API_VERSION + endpoint, 273 | 'nonce': self._get_nonce() 274 | } 275 | 276 | return self._invoke_api(endpoint, payload, pub=False) 277 | 278 | # Fund Management API's 279 | # https://docs.gemini.com/rest-api/#get-available-balances 280 | # -------------------------------------------------------- 281 | def get_balance(self): 282 | """ https://docs.gemini.com/rest-api/#get-available-balances """ 283 | endpoint = '/balances' 284 | 285 | payload = { 286 | 'request': self.API_VERSION + endpoint, 287 | 'nonce': self._get_nonce() 288 | } 289 | 290 | return self._invoke_api(endpoint, payload, pub=False) 291 | 292 | def new_deposit_address(self, currency, label): 293 | """ https://docs.gemini.com/rest-api/#new-deposit-address """ 294 | endpoint = '/deposit/' + currency + '/newAddress' 295 | 296 | payload = { 297 | 'request': self.API_VERSION + endpoint, 298 | 'nonce': self._get_nonce(), 299 | 'label': label 300 | } 301 | 302 | return self._invoke_api(endpoint, payload, pub=False) 303 | 304 | def withdraw_crypto(self, currency, address, amount): 305 | """ https://docs.gemini.com/rest-api/#withdraw-crypto-funds-to-whitelisted-address """ 306 | endpoint = '/withdraw/' + currency 307 | 308 | payload = { 309 | 'request': self.API_VERSION + endpoint, 310 | 'nonce': self._get_nonce(), 311 | 'address': address, 312 | 'amount': amount 313 | } 314 | 315 | return self._invoke_api(endpoint, payload, pub=False) 316 | 317 | def get_notional_volume(self): 318 | """ https://docs.gemini.com/rest-api/#get-notional-volume """ 319 | endpoint = '/notionalvolume' 320 | 321 | payload = { 322 | 'request': self.API_VERSION + endpoint, 323 | 'nonce': self._get_nonce(), 324 | } 325 | 326 | return self._invoke_api(endpoint, payload, pub=False) -------------------------------------------------------------------------------- /gemini/error.py: -------------------------------------------------------------------------------- 1 | class GeminiError(Exception): 2 | """ 3 | Basic exception class for errors raised by the API Library. 4 | 5 | https://docs.gemini.com/rest-api/#error-codes 6 | """ 7 | 8 | def __init__(self, message): 9 | self.message = message 10 | 11 | def __str__(self): 12 | return self.message 13 | 14 | class AuctionNotOpen(GeminiError): pass 15 | class ClientOrderIdTooLong(GeminiError): pass 16 | class ClientOrderIdMustBeString(GeminiError): pass 17 | class ConflictingOptions(GeminiError): pass 18 | class EndpointMismatch(GeminiError): pass 19 | class EndpointNotFound(GeminiError): pass 20 | class IneligibleTiming(GeminiError): pass 21 | class InsufficientFunds(GeminiError): pass 22 | class InvalidJson(GeminiError): pass 23 | class InvalidNonce(GeminiError): pass 24 | class InvalidOrderType(GeminiError): pass 25 | class InvalidPrice(GeminiError): pass 26 | class InvalidQuantity(GeminiError): pass 27 | class InvalidSide(GeminiError): pass 28 | class InvalidSignature(GeminiError): pass 29 | class InvalidSymbol(GeminiError): pass 30 | class Maintenance(GeminiError): pass 31 | class MarketNotOpen(GeminiError): pass 32 | class MissingApikeyHeader(GeminiError): pass 33 | class MissingOrderField(GeminiError): pass 34 | class MissingRole(GeminiError): pass 35 | class MissingPayloadHeader(GeminiError): pass 36 | class MissingSignatureHeader(GeminiError): pass 37 | class NoSSL(GeminiError): pass 38 | class OptionsMustBeArray(GeminiError): pass 39 | class OrderNotFound(GeminiError): pass 40 | class RateLimit(GeminiError): pass 41 | class System(GeminiError): pass 42 | class UnsupportedOption(GeminiError): pass 43 | 44 | api_error_map = { 45 | 'AuctionNotOpen': AuctionNotOpen, 46 | 'ClientOrderIDTooLong': ClientOrderIdTooLong, 47 | 'ClientOrderIDMustBeString': ClientOrderIdMustBeString, 48 | 'ConflictingOptions': ConflictingOptions, 49 | 'EndpointMismatch': EndpointMismatch, 50 | 'EndpointNotFound': EndpointNotFound, 51 | 'IneligibleTiming': IneligibleTiming, 52 | 'InsufficientFunds': InsufficientFunds, 53 | 'InvalidJson': InvalidJson, 54 | 'InvalidNonce': InvalidNonce, 55 | 'InvalidOrderType': InvalidOrderType, 56 | 'InvalidPrice': InvalidPrice, 57 | 'InvalidQuantity': InvalidQuantity, 58 | 'InvalidSide': InvalidSide, 59 | 'InvalidSignature': InvalidSignature, 60 | 'InvalidSymbol': InvalidSymbol, 61 | 'Maintenance': Maintenance, 62 | 'MarketNotOpen': MarketNotOpen, 63 | 'MissingApikeyHeader': MissingApikeyHeader, 64 | 'MissingOrderField': MissingOrderField, 65 | 'MissingRole': MissingRole, 66 | 'MissingPayloadHeader': MissingPayloadHeader, 67 | 'MissingSignatureHeader': MissingSignatureHeader, 68 | 'NoSSL': NoSSL, 69 | 'OptionsMustBeArray': OptionsMustBeArray, 70 | 'OrderNotFound': OrderNotFound, 71 | 'RateLimit': RateLimit, 72 | 'System': System, 73 | 'UnsupportedOption': UnsupportedOption 74 | } 75 | 76 | def raise_api_error(request, response): 77 | err_txt = response.json()['message'] + '(' + str(request) + ')' 78 | error = api_error_map.get(response.json()['reason']) 79 | 80 | return error(err_txt) 81 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.2 2 | packaging==16.8 3 | pyparsing==2.1.10 4 | requests==2.13.0 5 | six==1.10.0 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | import gemini 4 | 5 | setup( 6 | name='gemini-python-unoffc', 7 | version=gemini.__version__, 8 | packages=find_packages(), 9 | install_requires=['requests>=2.13.0'], 10 | author='Matt Selph', 11 | author_email='mattselph@outlook.com', 12 | description='An Unofficial Python library for the Gemini Exchange REST API.', 13 | license='MIT', 14 | keywords='bitcoin ethereum api gemini', 15 | url='https://github.com/mattselph/gemini-python-unoffc', 16 | classifiers=[ 17 | 'Intended Audience :: Developers', 18 | 'License :: OSI Approved :: MIT License', 19 | 'Operating System :: OS Independent', 20 | 'Programming Language :: Python :: 3', 21 | 'Programming Language :: Python :: 3.2', 22 | 'Programming Language :: Python :: 3.3', 23 | 'Programming Language :: Python :: 3.4', 24 | 'Programming Language :: Python :: 3.5', 25 | 'Topic :: Software Development :: Libraries :: Python Modules' 26 | ] 27 | ) 28 | --------------------------------------------------------------------------------