├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── __init__.py ├── examples ├── asksmarketdata.py └── print_spread.py ├── gemini ├── __init__.py ├── basewebsocket.py ├── cached.py ├── debugly.py ├── marketdataws.py ├── order_book.py ├── ordereventsws.py ├── private_client.py └── public_client.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── keys.py ├── test_marketdataws.py ├── test_ordereventsws.py ├── test_private_client.py └── test_public_client.py /.gitignore: -------------------------------------------------------------------------------- 1 | keys.txt 2 | 3 | *.pyc 4 | *~ 5 | *.swp 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at m.t.usman@hotmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ### Users can contribute to this repository in a variety of ways: 4 | 5 | #### Have a feature you want implemented? 6 | 7 | Open an [issue](https://github.com/mtusman/gemini-python-wrapper/issues) stating what feature you wish to implement, [pull requests](https://github.com/mtusman/gemini-python-wrapper/pulls) are encouraged and will speed up implementation. 8 | 9 | #### Found a bug that needs fixing? 10 | 11 | If you know how to fix it, a [pull request](https://github.com/mtusman/gemini-python-wrapper/pulls) is encouraged, otherwise open an issue providing details on the events leading up to the bug, including a traceback of the error. 12 | 13 | #### Want to improve documentation? 14 | 15 | Take a look at the [Wiki](https://github.com/mtusman/gemini-python-wrapper/wiki) and docstrings. Improvement is always welcome. 16 | 17 | #### Want to just show support? 18 | 19 | You can simply star/fork [this repository at github.com](https://github.com/mtusman/gemini-python-wrapper/), and/or donate to the following: 20 | 21 | ETH: ` 0x0287b1B0032Dc42c16640F71BA06F1A87C3a7101` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | 3 | RUN mkdir /code 4 | WORKDIR /code 5 | COPY . /code/ 6 | RUN pip install -r requirements.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Mohammad Usman 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gemini-python 2 | A python client for the Gemini API and Websocket 3 | 4 | ## Getting Started 5 | ### Installation 6 | ```python 7 | pip install gemini_python 8 | ``` 9 | ### PublicClient 10 | This endpoint doesn't require an api-key and can 11 | be used without having a Gemini account. This README 12 | will document some of the methods and features of the class. 13 | ```python 14 | import gemini 15 | r = gemini.PublicClient() 16 | # Alternatively, for a sandbox environment, set sandbox=True 17 | r = gemini.PublicClient(sandbox=True) 18 | ``` 19 | #### PublicClient Methods 20 | - [symbols](https://docs.gemini.com/rest-api/#symbols) 21 | ```python 22 | r.symbols() 23 | ``` 24 | - [symbol_details](https://docs.gemini.com/rest-api/#symbol-details) 25 | ```python 26 | r.symbol_details("BTCUSD") 27 | ``` 28 | - [get_ticker](https://docs.gemini.com/rest-api/#ticker) 29 | ```python 30 | r.get_ticker("BTCUSD") 31 | ``` 32 | - [get_current_order_book](https://docs.gemini.com/rest-api/#current-order-book) 33 | ```python 34 | r.get_current_order_book("BTCUSD") 35 | ``` 36 | - [get_trade_history](https://docs.gemini.com/rest-api/#trade-history) 37 | ```python 38 | # Will get the latest 500 trades 39 | r.get_trade_history("BTCUSD") 40 | # Alternatively, it can be specified for a specific date 41 | r.get_trade_history("BTCUSD", since="17/06/2017") 42 | ``` 43 | - [get_auction_history](https://docs.gemini.com/rest-api/#current-auction) 44 | ```python 45 | # Will get the latest 500 auctions 46 | r.get_auction_history("BTCUSD") 47 | # Alternatively, it can be specified for a specific date 48 | r.get_auction_history("BTCUSD", since="17/06/2017") 49 | ``` 50 | 51 | ### PrivateClient 52 | This endpoint requires both a public and private key to access 53 | the API. Hence, one must have an account with Gemini and register an 54 | application. So far, if the 'heartbeat' option is enabled for the API, 55 | the user must manually revive the heartbeat. Further options will be added 56 | in the future in order to avoid doing this manually. 57 | 58 | The payload of the requests 59 | will be a JSON object. Rather than being sent as the body of the POST request, 60 | Gemini requires it to be base-64 encoded and stored as a header in the request. 61 | Adding a 'nonce' is optional for the API but is highly recommended. That's why 62 | the class will always send each request with a unique 'nonce'. An important 63 | point to note is that every argument for the methods of PrivateClient must be 64 | strings with the exception of 'options'. 65 | 66 | ```python 67 | import gemini 68 | r = gemini.PrivateClient("EXAMPLE_PUBLIC_KEY", "EXAMPLE_PRIVATE_KEY") 69 | # Alternatively, for a sandbox environment, set sandbox=True 70 | r = gemini.PrivateClient("EXAMPLE_PUBLIC_KEY", "EXAMPLE_PRIVATE_KEY", sandbox=True) 71 | ``` 72 | 73 | #### PrivateClient Methods 74 | - [new_order](https://docs.gemini.com/rest-api/#new-order) 75 | ```python 76 | r.new_order("BTCUSD", "200", "6000", "buy") 77 | ``` 78 | - [cancel_order](https://docs.gemini.com/rest-api/#cancel-order) 79 | ```python 80 | r.cancel_order("866403510") 81 | ``` 82 | - [wrap_order](https://docs.gemini.com/rest-api/#wrap-order) 83 | ```python 84 | r.wrap_order("GUSDUSD", "10", "buy") 85 | ``` 86 | - [cancel_session_orders](https://docs.gemini.com/rest-api/#cancel-all-session-orders) 87 | ```python 88 | r.cancel_session_orders() 89 | ``` 90 | - [cancel_all_orders](https://docs.gemini.com/rest-api/#cancel-all-active-orders) 91 | ```python 92 | r.cancel_all_orders() 93 | ``` 94 | - [status_of_order](https://docs.gemini.com/rest-api/#order-status) 95 | ```python 96 | r.status_of_order("866403510") 97 | ``` 98 | - [active_orders](https://docs.gemini.com/rest-api/#get-active-orders) 99 | ```python 100 | r.active_orders() 101 | ``` 102 | - [get_past_trades](https://docs.gemini.com/rest-api/#get-past-trades) 103 | ```python 104 | # Will get the last 500 past trades 105 | r.get_past_trades("BTCUSD") 106 | # Alternatively, you can set the limit_trades number to your liking 107 | r.get_past_trades("BTCUSD", limit_trades="200") 108 | ``` 109 | - [get_trade_volume](https://docs.gemini.com/rest-api/#get-trade-volume) 110 | ```python 111 | r.get_trade_volume() 112 | ``` 113 | - [get_balance](https://docs.gemini.com/rest-api/#get-available-balances) 114 | ```python 115 | r.get_balance() 116 | ``` 117 | - [create_deposit_address](https://docs.gemini.com/rest-api/#new-deposit-address) 118 | ```python 119 | # This will create a new currency address 120 | r.create_deposit_address("BTCUSD") 121 | # Alternatively, you can specify the label 122 | r.create_deposit_address("BTCUSD", label="Main Bitcoin Address") 123 | ``` 124 | - [withdraw_to_address](https://docs.gemini.com/rest-api/#withdraw-crypto-funds-to-whitelisted-address) 125 | ```python 126 | r.withdraw_to_address("ETH", "0x0287b1B0032Dc42c16640F71BA06F1A87C3a7101", "20") 127 | ``` 128 | - [revive_hearbeat](https://docs.gemini.com/rest-api/#ticker) 129 | ```python 130 | r.revive_hearbeat() 131 | ``` 132 | ### Websocket Client 133 | If you'd prefer to recieve live updates you can either choose to subsribe to the public market data websocket or the private order events websocket. For more information about the difference between the two websockets visit the official [Gemini documentation](https://docs.gemini.com/websocket-api). 134 | 135 | ### MarketData Websocket 136 | Market data is a public API that streams all the market data on a given symbol. 137 | ```python 138 | import gemini 139 | r = gemini.MarketDataWS('btcusd') 140 | # Alternatively, for a sandbox environment, set sandbox=True 141 | r = gemini.MarketDataWS('btcusd', sandbox=True) 142 | ``` 143 | #### MarketData Websocket Methods 144 | - get list of recorded trades 145 | ```python 146 | r.trades 147 | ``` 148 | - get recorded bids 149 | ```python 150 | r.bids 151 | ``` 152 | - get recorded asks 153 | ```python 154 | r.asks 155 | ``` 156 | - get market book 157 | ```python 158 | r.get_market_book() 159 | ``` 160 | - remove a recorded price from bids or asks 161 | ```python 162 | # To remove a price from bids 163 | r.remove_from_bids('10000') 164 | # To remove a price from asks 165 | r.remove_from_asks('10000') 166 | ``` 167 | - search for a particular price recorded 168 | ```python 169 | r.search_price('10000') 170 | ``` 171 | - export recorded trades to csv 172 | ```python 173 | r.export_to_csv(r'/c/Users/user/Documents') 174 | ``` 175 | - export recorded trades to xml 176 | ```python 177 | r.export_to_xml(r'/c/Users/user/Documents') 178 | ``` 179 | ### OrderEvents Websocket 180 | Order events is a private API that gives you information about your orders in real time.When you connect, you get a book of your active orders. Then in real time you'll get information about order events like: 181 | 182 | - when your orders are accepted by the exchange 183 | - when your orders first appear on the book 184 | - fills 185 | - cancels 186 | - and more. 187 | 188 | 189 | Support for subscription filters is currently under development 190 | 191 | ```python 192 | import gemini 193 | r = gemini.OrderEventsWS("EXAMPLE_PUBLIC_KEY", "EXAMPLE_PRIVATE_KEY") 194 | # Alternatively, for a sandbox environment, set sandbox=True 195 | r = gemini.OrderEventsWS("EXAMPLE_PUBLIC_KEY", "EXAMPLE_PRIVATE_KEY", sandbox=True) 196 | ``` 197 | 198 | #### OrderEvents Websocket Methods 199 | - get order types 200 | ```python 201 | """All trades are categorised in terms of either subscription_ack', 'heartbeat', 202 | 'initial', 'accepted','rejected', 'booked', 'fill', 'cancelled', 203 | 'cancel_rejected' or 'closed'. The following will print these types""" 204 | r.get_order_types 205 | ``` 206 | - get order book 207 | ```python 208 | # Will return all recorded orders 209 | r.get_order_book 210 | ``` 211 | - remove a recorded price from the order book 212 | ```python 213 | # Arguments are: type and order_id 214 | r.remove_order('accepted', '12321123') 215 | ``` 216 | - export recorded trades to csv 217 | ```python 218 | # Arguments are: directory and type 219 | # The following will export all 'accepted' orders to a csv format 220 | r.export_to_csv(r'/c/Users/user/Documents', 'accepted') 221 | ``` 222 | - export recorded trades to xml 223 | ```python 224 | # Arguments are: directory and type. 225 | # The following will export all 'accepted' orders to a xml format 226 | r.export_to_xml(r'/c/Users/user/Documents', 'accepted') 227 | ``` 228 | 229 | # Under Development 230 | - Add filter options to order events websocket 231 | - Improve options to add and remove orders from market data websocket 232 | - Add options to choose whether a particular class is cached or not 233 | - Export recorded data from market data or order events websocket into a matplotlib graph 234 | - Export recorded data from market data or order events websocket into a sqlite, postgresl or sql database 235 | - Add test for the cached metaclass 236 | 237 | # Change Log 238 | *0.2.0* 239 | - Created BaseWebsocket class 240 | - Created OrderEventsWS class to interact with the order events websocket 241 | - Created MarketDataWS class to interact with the market data websocket 242 | - Added greater support for heartbeat API's 243 | - Improved the Cached metaclass 244 | - Added support for sandbox urls 245 | 246 | *0.0.1* 247 | - Original release -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtusman/gemini-python/a52da3d7eba64ebb342c969b751c1e52d83e2510/__init__.py -------------------------------------------------------------------------------- /examples/asksmarketdata.py: -------------------------------------------------------------------------------- 1 | # askmarketdata.py 2 | # Mohammad Usman 3 | # 4 | # A simple example showing how the BaseWebSocket class can be modified. 5 | # AsksWebSocket will print the latest 100 ask orders and then close the 6 | # connection 7 | 8 | import sys 9 | sys.path.insert(0, '..') 10 | from gemini.basewebsocket import BaseWebSocket 11 | from collections import deque 12 | 13 | 14 | class AsksWebSocket(BaseWebSocket): 15 | def __init__(self, base_url): 16 | super().__init__(base_url) 17 | self._asks = deque(maxlen=100) 18 | 19 | def on_open(self): 20 | print('--Subscribed to asks orders!--\n') 21 | 22 | def on_message(self, msg): 23 | try: 24 | event = msg['events'][0] 25 | if event['type'] == 'trade' and event['makerSide'] == 'ask': 26 | print(msg) 27 | self._asks.append(msg['events']) 28 | self.messages += 1 29 | except KeyError as e: 30 | pass 31 | 32 | 33 | if __name__ == '__main__': 34 | wsClient = AsksWebSocket('wss://api.gemini.com/v1/marketdata/btcusd') 35 | wsClient.start() 36 | while True: 37 | if wsClient.messages >= 100: 38 | wsClient.close() 39 | break 40 | -------------------------------------------------------------------------------- /examples/print_spread.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import time 4 | 5 | from gemini.order_book import GeminiOrderBook 6 | 7 | book = GeminiOrderBook('ETHUSD') 8 | book.start() 9 | time.sleep(5) # needs a bit of time to populate the order book 10 | while(True): 11 | print("Spread: %f" % (book.get_ask() - book.get_bid())) 12 | time.sleep(1) 13 | -------------------------------------------------------------------------------- /gemini/__init__.py: -------------------------------------------------------------------------------- 1 | from .public_client import PublicClient 2 | from .private_client import PrivateClient 3 | from .basewebsocket import BaseWebSocket 4 | from .marketdataws import MarketDataWS 5 | from .ordereventsws import OrderEventsWS 6 | from .order_book import GeminiOrderBook 7 | -------------------------------------------------------------------------------- /gemini/basewebsocket.py: -------------------------------------------------------------------------------- 1 | # basewebsocket.py 2 | # Mohammad Usman 3 | # 4 | # This class is to be used as the parent for the MarketWebsocket and 5 | # OrderWebsocket 6 | from .cached import Cached 7 | from .debugly import typeassert 8 | from threading import Thread 9 | from websocket import create_connection, WebSocketConnectionClosedException 10 | import json 11 | 12 | 13 | class BaseWebSocket(metaclass=Cached): 14 | @typeassert(base_url=str) 15 | def __init__(self, base_url): 16 | self.base_url = base_url 17 | self.ws = None 18 | self.messages = 0 19 | 20 | def start(self): 21 | def _go(): 22 | self._connect() 23 | self._listen() 24 | self._disconnect() 25 | self.stop = False 26 | self.on_open() 27 | self.thread = Thread(target=_go) 28 | self.thread.start() 29 | 30 | def _connect(self): 31 | self.ws = create_connection(self.base_url) 32 | 33 | def _listen(self): 34 | while not self.stop: 35 | try: 36 | data = self.ws.recv() 37 | except ValueError as e: 38 | self.on_error(e) 39 | except Exception as e: 40 | self.on_error(e) 41 | else: 42 | self.on_message(json.loads(data)) 43 | 44 | def _disconnect(self): 45 | try: 46 | if self.ws: 47 | self.ws.close() 48 | self.on_close() 49 | except WebSocketConnectionClosedException as e: 50 | self.on_error(e) 51 | 52 | def close(self): 53 | self.stop = True 54 | self.thread.join() 55 | 56 | def on_open(self): 57 | print('--Subscribed--\n') 58 | 59 | def on_message(self, msg): 60 | print(msg) 61 | self.messages += 1 62 | 63 | def on_error(self, e): 64 | print(e) 65 | 66 | def on_close(self): 67 | print('\n--Ended Connection--') 68 | -------------------------------------------------------------------------------- /gemini/cached.py: -------------------------------------------------------------------------------- 1 | # cached.py 2 | # Mohammad Usman 3 | # 4 | # A metaclass that creates catched instances. 5 | 6 | import weakref 7 | 8 | 9 | class Cached(type): 10 | def __init__(self, *args, **kwargs): 11 | self.__cache = weakref.WeakValueDictionary() 12 | super().__init__(*args, **kwargs) 13 | 14 | def __call__(self, *args, **kwargs): 15 | if kwargs == {'sandbox': False}: 16 | key = args + (False,) 17 | elif kwargs == {'sandbox': True}: 18 | key = args + (True,) 19 | else: 20 | if len(args) == 1 or len(args) == 3: 21 | key = args 22 | else: 23 | key = args + (False,) 24 | if key in self.__cache: 25 | return self.__cache[key] 26 | else: 27 | obj = super().__call__(*args, **kwargs) 28 | self.__cache[key] = obj 29 | return obj 30 | -------------------------------------------------------------------------------- /gemini/debugly.py: -------------------------------------------------------------------------------- 1 | # dubugly.py 2 | # Mohammad Usman 3 | # 4 | # A collection of useful decorators 5 | 6 | from inspect import signature 7 | from functools import wraps 8 | 9 | 10 | # Should only be used on functions/methods that don't have keyword arguments 11 | def typeassert(*ty_args, **ty_kwargs): 12 | def decorate(func): 13 | # Map function argument names to supplied types 14 | sig = signature(func) 15 | bound_types = sig.bind_partial(*ty_args, **ty_kwargs).arguments 16 | 17 | @wraps(func) 18 | def wrapper(*args, **kwargs): 19 | bound_values = sig.bind(*args, **kwargs) 20 | # Enforce type assertions across supplied arguments 21 | for name, value in bound_values.arguments.items(): 22 | if name in bound_types: 23 | if not isinstance(value, bound_types[name]): 24 | raise TypeError( 25 | 'Argument {} must be {}'.format(name, bound_types[name]) 26 | ) 27 | return func(*args, **kwargs) 28 | return wrapper 29 | return decorate 30 | -------------------------------------------------------------------------------- /gemini/marketdataws.py: -------------------------------------------------------------------------------- 1 | # marketdataws.py 2 | # Mohammad Usman 3 | # 4 | # A python wrapper for Gemini's market data websocket 5 | 6 | from .basewebsocket import BaseWebSocket 7 | from .debugly import typeassert 8 | from collections import OrderedDict 9 | from xml.etree.ElementTree import Element, tostring 10 | from xml.dom import minidom 11 | import os 12 | import csv 13 | 14 | 15 | class MarketDataWS(BaseWebSocket): 16 | """ 17 | Market data is a public API that streams all the market data on a 18 | given symbol. 19 | """ 20 | @typeassert(product_id=str, sandbox=bool) 21 | def __init__(self, product_id, sandbox=False): 22 | if sandbox: 23 | super().__init__(base_url='wss://api.sandbox.gemini.com/v1/marketdata/{}' 24 | .format(product_id)) 25 | else: 26 | super().__init__(base_url='wss://api.gemini.com/v1/marketdata/{}' 27 | .format(product_id)) 28 | 29 | self.product_id = product_id 30 | self.asks = OrderedDict() 31 | self.bids = OrderedDict() 32 | self.trades = [] 33 | 34 | def on_message(self, msg): 35 | """ 36 | Each msg will be a dict with the following keys: 'type', 37 | 'eventId','socket_sequence', 'timestamp' and 'events'. 38 | 39 | 'events' will be a list of dict's with each dict containing 40 | the following keys: 'type', 'tid', 'price', 'amount' and 41 | 'makerSide'. If the first element of the list has type 'trade' 42 | then the method will append the trade to self.trades and add 43 | the event to either bids or asks depending on the 'makerSide'. 44 | """ 45 | if msg['socket_sequence'] >= 1: 46 | event = msg['events'][0] 47 | if event['type'] == 'trade': 48 | self.trades.append(event) 49 | self.add(event['makerSide'], msg) 50 | 51 | @typeassert(side=str) 52 | def add(self, side, msg): 53 | """ 54 | This method will create a custom order dict by extracting 55 | the appropriate information from the msg retrieved and then place the 56 | dict to either self.bids or self.asks depending on the 'makerSide'. 57 | 58 | Args: 59 | side(str): Either "buy" or "ask" 60 | msg(dict): Dict with keys: 'type','eventId','socket_sequence', 61 | 'timestamp' and 'events' 62 | """ 63 | trade_event = msg['events'][0] 64 | order = { 65 | 'eventId': msg['eventId'], 66 | 'timestamp': msg['timestamp'], 67 | 'price': trade_event['price'], 68 | 'amount': trade_event['amount'], 69 | 'makerSide': trade_event['makerSide'] 70 | } 71 | if trade_event['makerSide'] == 'bid': 72 | self.add_to_bids(trade_event['price'], order) 73 | else: 74 | self.add_to_asks(trade_event['price'], order) 75 | 76 | def get_market_book(self): 77 | result = { 78 | 'asks': self.asks, 79 | 'bids': self.bids 80 | } 81 | return result 82 | 83 | def reset_market_book(self): 84 | self.asks, self.bids = OrderedDict(), OrderedDict() 85 | print('Market book reset to empty') 86 | 87 | @typeassert(price=str) 88 | def search_price(self, price): 89 | """ 90 | Will return the all the trades on either asks or bids with the 91 | given price. 92 | 93 | Args: 94 | price(str): Must already be in self.asks or self.bids 95 | """ 96 | if price in self.asks and price in self.bids: 97 | result = {'price': self.asks[price].extend(self.bids[price])} 98 | elif price in self.asks: 99 | result = {'price': self.asks[price]} 100 | elif price in self.bids: 101 | result = {'price': self.bids[price]} 102 | else: 103 | result = {'price': []} 104 | return result 105 | 106 | @typeassert(price=str, order=dict) 107 | def add_to_bids(self, price, order): 108 | """ 109 | Allows the user to manually add an order to bids given it's 110 | an appropriate dict. 111 | 112 | Args: 113 | price(str): Must already be in self.asks or self.bids 114 | order(str): Dict with keys: 'eventId','socket_sequence','timestamp' 115 | and 'events' 116 | """ 117 | if ('eventId' and 'timestamp' and 'price' and 'amount' and 118 | 'makerSide') in order: 119 | if price in self.bids.keys(): 120 | self.bids[price].append(order) 121 | else: 122 | self.bids[price] = [order] 123 | else: 124 | print("Orders must be a dict with the following keys: 'eventId', " 125 | "'timestamp', 'price', 'amount' and 'makerSide'") 126 | 127 | @typeassert(price=str) 128 | def remove_from_bids(self, price): 129 | try: 130 | del self.bids[price] 131 | except KeyError as e: 132 | print('No order with price {} found'.format(price)) 133 | 134 | @typeassert(price=str, order=dict) 135 | def add_to_asks(self, price, order): 136 | """ 137 | Allows the user to manually asks an order to bids given it's 138 | an appropriate dict. 139 | 140 | Args: 141 | price(str): Must already be in self.asks or self.bids 142 | order(dict): Dict with keys: 'eventId','socket_sequence','timestamp' 143 | and 'events' 144 | """ 145 | if ('eventId' and 'timestamp' and 'price' and 'amount' and 146 | 'makerSide') in order: 147 | if price in self.asks.keys(): 148 | self.asks[price].append(order) 149 | else: 150 | self.asks[price] = [order] 151 | else: 152 | print("Orders enter manually must be a dict with the following " 153 | "keys: eventId, timestamp, price, amount and makerSide") 154 | 155 | @typeassert(price=str) 156 | def remove_from_asks(self, price): 157 | try: 158 | del self.asks[price] 159 | except KeyError as e: 160 | print('No order with price {} found'.format(price)) 161 | 162 | @typeassert(dir=str, newline_selection=str) 163 | def export_to_csv(self, dir, newline_selection=''): 164 | """ Will export the trades recorded into a csv file. 165 | Note: directory for the file to be saved must be given as raw input. 166 | 167 | Args: 168 | dir(str): Must be in raw string 169 | newline_selection(str): Default value is '' 170 | """ 171 | headers = ['type', 'tid', 'price', 'amount', 'makerSide'] 172 | with open(os.path.join(r'{}'.format(dir), 'gemini_market_data.csv'), 173 | 'w', 174 | newline=newline_selection) as f: 175 | f_csv = csv.DictWriter(f, headers) 176 | f_csv.writeheader() 177 | f_csv.writerows(self.trades) 178 | 179 | def _trades_to_xml(self): 180 | """ 181 | Turn a list of dicts into XML. 182 | """ 183 | parent_elem = Element('trades') 184 | for trade in self.trades: 185 | trade_elem = Element('trade') 186 | for key, val in trade.items(): 187 | child = Element(key) 188 | child.text = str(val) 189 | trade_elem.append(child) 190 | parent_elem.append(trade_elem) 191 | return parent_elem 192 | 193 | @typeassert(dir=str) 194 | def export_to_xml(self, dir): 195 | """ 196 | Will export the trades recorded into a xml file. 197 | Note: directory for the file to be saved must be given as raw input. 198 | Args: 199 | dir(str): Must be in raw string 200 | """ 201 | rough_string = tostring(self._trades_to_xml(), 'utf-8') 202 | reparsed = minidom.parseString(rough_string).toprettyxml(indent=" ") 203 | with open(os.path.join(r'{}'.format(dir), 'gemini_market_data.xml'), 204 | 'w') as f: 205 | f.write(reparsed) 206 | -------------------------------------------------------------------------------- /gemini/order_book.py: -------------------------------------------------------------------------------- 1 | # order_book.py 2 | # Sebastian Quilter 3 | # 4 | # A python wrapper for Gemini which keeps an updated order book 5 | 6 | import time 7 | 8 | from .basewebsocket import BaseWebSocket 9 | from .debugly import typeassert 10 | 11 | class GeminiOrderBook(BaseWebSocket): 12 | """ 13 | Market data is a public API that streams all the market data on a 14 | given symbol. 15 | """ 16 | @typeassert(product_id=str, sandbox=bool) 17 | def __init__(self, product_id, sandbox=False): 18 | if sandbox: 19 | super().__init__(base_url='wss://api.sandbox.gemini.com/v1/marketdata/{}' 20 | .format(product_id)) 21 | else: 22 | super().__init__(base_url='wss://api.gemini.com/v1/marketdata/{}' 23 | .format(product_id)) 24 | 25 | self.product_id = product_id 26 | self.asks = {} 27 | self.bids = {} 28 | 29 | def on_message(self, msg): 30 | if msg['socket_sequence'] >= 1: 31 | for event in msg['events']: 32 | if event['type'] == 'change': 33 | price = float(event['price']) 34 | remaining = float(event['remaining']) 35 | if(event['side'] == 'ask'): 36 | if(remaining == 0.0 and price in self.asks): 37 | self.asks.pop(price) 38 | elif(remaining != 0.0): 39 | self.asks[price] = remaining 40 | elif(event['side'] == 'bid'): 41 | if(remaining == 0.0 and price in self.bids): 42 | self.bids.pop(price) 43 | elif(remaining != 0.0): 44 | self.bids[price] = remaining 45 | 46 | def get_ask(self): 47 | return min(self.asks.keys()) 48 | 49 | def get_bid(self): 50 | return max(self.bids.keys()) 51 | 52 | def get_market_book(self): 53 | result = { 54 | 'asks': self.asks, 55 | 'bids': self.bids 56 | } 57 | return result 58 | 59 | def reset_market_book(self): 60 | self.asks, self.bids = {}, {} 61 | print('Market book reset to empty') 62 | 63 | -------------------------------------------------------------------------------- /gemini/ordereventsws.py: -------------------------------------------------------------------------------- 1 | # ordereventsws.py 2 | # Mohammad Usman 3 | # 4 | # A python wrapper for Gemini's order events websocket 5 | # This is a private endpoint and so requires a private and public keys 6 | 7 | from .basewebsocket import BaseWebSocket 8 | from .debugly import typeassert 9 | from websocket import create_connection 10 | from collections import OrderedDict 11 | from xml.etree.ElementTree import Element, tostring 12 | from xml.dom import minidom 13 | import os 14 | import csv 15 | import json 16 | import hmac 17 | import hashlib 18 | import base64 19 | import time 20 | 21 | 22 | class OrderEventsWS(BaseWebSocket): 23 | @typeassert(PUBLIC_API_KEY=str, PRIVATE_API_KEY=str, sandbox=bool) 24 | def __init__(self, PUBLIC_API_KEY, PRIVATE_API_KEY, sandbox=False): 25 | if sandbox: 26 | super().__init__(base_url='wss://api.sandbox.gemini.com/v1/order/events') 27 | else: 28 | super().__init__(base_url='wss://api.gemini.com/v1/order/events') 29 | self._public_key = PUBLIC_API_KEY 30 | self._private_key = PRIVATE_API_KEY 31 | self.order_book = OrderedDict() 32 | self._reset_order_book() 33 | 34 | @property 35 | def get_order_types(self): 36 | print("Order types are: subscription_ack', 'heartbeat', 'initial', " 37 | "'accepted','rejected', 'booked', 'fill', 'cancelled', " 38 | "cancel_rejected' or 'closed'") 39 | 40 | def _reset_order_book(self): 41 | """ 42 | Will create a dict with all the following msg types to be received 43 | by the websocket. 44 | """ 45 | order_types = ['subscription_ack', 'heartbeat', 'initial', 'accepted', 46 | 'rejected', 'booked', 'fill', 'cancelled', 47 | 'cancel_rejected', 'closed'] 48 | for order_type in order_types: 49 | self.order_book[order_type] = list() 50 | 51 | @typeassert(method=str, payload=dict) 52 | def api_query(self, method, payload=None): 53 | if payload is None: 54 | payload = {} 55 | payload['request'] = method 56 | payload['nonce'] = int(time.time() * 1000) 57 | b64_payload = base64.b64encode(json.dumps(payload).encode('utf-8')) 58 | signature = hmac.new(self._private_key.encode('utf-8'), 59 | b64_payload, hashlib.sha384).hexdigest() 60 | 61 | headers = { 62 | 'X-GEMINI-APIKEY': self._public_key, 63 | 'X-GEMINI-PAYLOAD': b64_payload.decode('utf-8'), 64 | 'X-GEMINI-SIGNATURE': signature 65 | } 66 | return headers 67 | 68 | def _connect(self): 69 | self.ws = create_connection(self.base_url, 70 | header=self.api_query('/v1/order/events'), 71 | skip_utf8_validation=True) 72 | 73 | def on_message(self, msg): 74 | """ 75 | Each msg will either be a list or a dict. The first msg to be 76 | recieved with be a subscription acknowledgement with the following 77 | keys: 'type', 'accountId', 'subscriptionId', 'symbolFilter', 78 | 'apiSessionFilter' and 'eventTypeFilter'. 79 | 80 | Any messages recieved further will be of two types: either a heartbeat 81 | or a list of events. Gemini will send a hearbeat every 5 seconds and 82 | recommends the user store all hearbeats. Each list of events will have 83 | the following keys: 'type', 'socket_sequence', 'order_id', 'event_id', 84 | 'api_session', 'client_order_id', 'symbol', 'side', 'behavior', 85 | 'order_type', 'timestamp', 'timestampms', 'is_live', 'is_cancelled', 86 | 'is_hidden', 'avg_execution_price', 'executed_amount', 87 | 'remaining_amount', 'original_amount', 'price' and 'total_spend'. This 88 | method will check the type of any orders and assign them to their 89 | appropriate keys within self.order_book. 90 | """ 91 | if isinstance(msg, list): 92 | for order in msg: 93 | self.order_book[order['type']].append(order) 94 | elif msg['type'] == 'subscription_ack': 95 | self.order_book['subscription_ack'].append(msg) 96 | elif msg['type'] == 'heartbeat': 97 | self.order_book['heartbeat'].append(msg) 98 | else: 99 | pass 100 | 101 | def get_order_book(self): 102 | return self.order_book 103 | 104 | def reset_order_book(self): 105 | self._reset_order_book() 106 | print('Order book reset to empty') 107 | 108 | @typeassert(type=str, order_id=str) 109 | def remove_order(self, type, order_id): 110 | """ 111 | Will remove a order given a type within self.order_book and the 112 | orders id number. 113 | 114 | Args: 115 | type(str): Can be any value in self.get_order_types 116 | order_id(str): Must already be in self.order_book[type] 117 | """ 118 | order_type = self.order_book[type] 119 | for index, order in enumerate(order_type): 120 | if order['order_id'] == order_id: 121 | pop_index = index 122 | try: 123 | del order_type[pop_index] 124 | print('Deleted order with order_id:{}'.format(order_id)) 125 | except NameError as e: 126 | print('Order with order_id:{} does not exist '.format(order_id)) 127 | 128 | @typeassert(dir=str, type=str, newline_selection=str) 129 | def export_to_csv(self, dir, type, newline_selection=''): 130 | """ 131 | Will export the orders of a specific type to a csv format. If the 132 | type given is not in self.order_book then it'll print an error message 133 | with instructions of the appropriate types to be entered. 134 | Note: directory for the file to be saved must be given as raw input 135 | 136 | Args: 137 | dir(str): Must be in raw string 138 | type(str): Can be any valie in self.get_order_types 139 | newline_selection(str): Default value is '' 140 | """ 141 | if type in self.order_book.keys(): 142 | order_type = self.order_book[type] 143 | if len(order_type) >= 1: 144 | headers = order_type[0].keys() 145 | with open(os.path.join(r'{}'.format(dir), 'gemini_order_events.csv'), 146 | 'w', 147 | newline=newline_selection) as f: 148 | f_csv = csv.DictWriter(f, headers) 149 | f_csv.writeheader() 150 | f_csv.writerows(order_type) 151 | print('Successfully exported to csv') 152 | else: 153 | print('No order with type {} recorded'.format(type)) 154 | else: 155 | print("Type {} does not exist. Please select from: " 156 | "'subscription_ack', 'heartbeat', 'initial', 'accepted', " 157 | "'rejected', 'booked', 'fill', 'cancelled', " 158 | "cancel_rejected' or 'closed'".format(type)) 159 | 160 | def _trades_to_xml(self, type): 161 | """ 162 | Turn a list of dicts into XML. 163 | """ 164 | order_type = self.order_book[type] 165 | parent_elem = Element(type + 'orders') 166 | for trade in order_type: 167 | trade_elem = Element(type) 168 | for key, val in trade.items(): 169 | child = Element(key) 170 | child.text = str(val) 171 | trade_elem.append(child) 172 | parent_elem.append(trade_elem) 173 | return parent_elem 174 | 175 | @typeassert(dir=str, type=str) 176 | def export_to_xml(self, dir, type): 177 | """ 178 | Will export the orders of a specific type to a xml format. If the 179 | type given is not in self.order_book then it'll print an error message 180 | with instructions of the appropriate types to be entered. 181 | Note: directory for the file to be saved must be given as raw input. 182 | 183 | Args: 184 | dir(str): Must be in raw string 185 | type(str): Can be any valie in self.get_order_types 186 | """ 187 | if type in self.order_book.keys(): 188 | if len(self.order_book[type]) >= 1: 189 | rough_string = tostring(self._trades_to_xml(type), 'utf-8') 190 | reparsed = minidom.parseString(rough_string).toprettyxml(indent=" ") 191 | with open(os.path.join(r'{}'.format(dir), 'gemini_order_events.xml'), 192 | 'w') as f: 193 | f.write(reparsed) 194 | print('Successfully exported to xml') 195 | else: 196 | print('No order with type {} recorded'.format(type)) 197 | else: 198 | print("Type {} does not exist. Please select from: " 199 | "'subscription_ack', 'heartbeat', 'initial', 'accepted', " 200 | "'rejected', 'booked', 'fill', 'cancelled', " 201 | "cancel_rejected' or 'closed'".format(type)) 202 | -------------------------------------------------------------------------------- /gemini/private_client.py: -------------------------------------------------------------------------------- 1 | # private_client.py 2 | # Mohammad Usman 3 | # 4 | # A python wrapper for Gemini's public API 5 | 6 | from .public_client import PublicClient 7 | from .debugly import typeassert 8 | import requests 9 | import json 10 | import hmac 11 | import hashlib 12 | import base64 13 | import time 14 | 15 | 16 | class PrivateClient(PublicClient): 17 | @typeassert(PUBLIC_API_KEY=str, PRIVATE_API_KEY=str, sandbox=bool) 18 | def __init__(self, PUBLIC_API_KEY, PRIVATE_API_KEY, sandbox=False): 19 | super().__init__(sandbox) 20 | self._public_key = PUBLIC_API_KEY 21 | self._private_key = PRIVATE_API_KEY 22 | if sandbox: 23 | self._base_url = 'https://api.sandbox.gemini.com' 24 | else: 25 | self._base_url = 'https://api.gemini.com' 26 | 27 | @typeassert(method=str, payload=dict) 28 | def api_query(self, method, payload=None): 29 | if payload is None: 30 | payload = {} 31 | request_url = self._base_url + method 32 | 33 | payload['request'] = method 34 | payload['nonce'] = int(time.time() * 1000) 35 | b64_payload = base64.b64encode(json.dumps(payload).encode('utf-8')) 36 | signature = hmac.new(self._private_key.encode('utf-8'), b64_payload, hashlib.sha384).hexdigest() 37 | 38 | headers = { 39 | 'Content-Type': "text/plain", 40 | 'Content-Length': "0", 41 | 'X-GEMINI-APIKEY': self._public_key, 42 | 'X-GEMINI-PAYLOAD': b64_payload, 43 | 'X-GEMINI-SIGNATURE': signature, 44 | 'Cache-Control': "no-cache" 45 | } 46 | 47 | r = requests.post(request_url, headers=headers) 48 | return r.json() 49 | 50 | # Order Placement API 51 | @typeassert(symbol=str, amount=str, price=str, side=str, options=list) 52 | def new_order(self, symbol, amount, price, side, options=["immediate-or-cancel"]): 53 | """ 54 | This endpoint is used for the creation of a new order. 55 | Requires you to provide the symbol, amount, price, side and options. 56 | Options is an array and should include on the following: 57 | "maker-or-cancel","immediate-or-cancel", auction-only" 58 | So far Gemini only supports "type" as "exchange limit". 59 | 60 | Args: 61 | product_id(str): Can be any value in self.symbols() 62 | amount(str): The amount of currency you want to buy. 63 | price(str): The price at which you want to buy the currency/ 64 | side(str): Either "buy" or "ask" 65 | options(list): Currently, can only be ["immediate-or-cancel"] 66 | 67 | Returns: 68 | dict: These are the same fields returned by order/status 69 | example: { 70 | 'order_id': '86403510', 71 | 'id': '86403510', 72 | 'symbol': 'btcusd', 73 | 'exchange': 'gemini', 74 | 'avg_execution_price': '0.00', 75 | 'side': 'buy', 76 | 'type': 'exchange limit', 77 | 'timestamp': '1510403257', 78 | 'timestampms': 1510403257453, 79 | 'is_live': True, 80 | 'is_cancelled': False, 81 | 'is_hidden': False, 82 | 'was_forced': False, 83 | 'executed_amount': '0', 84 | 'remaining_amount': '0.02', 85 | 'options': ['maker-or-cancel'], 86 | 'price': '6400.28', 87 | 'original_amount': '0.02' 88 | } 89 | """ 90 | payload = { 91 | 'symbol': symbol, 92 | 'amount': amount, 93 | 'price': price, 94 | 'side': side, 95 | 'options': options, 96 | 'type': 'exchange limit' 97 | } 98 | return self.api_query('/v1/order/new', payload) 99 | 100 | @typeassert(order_id=str) 101 | def cancel_order(self, order_id): 102 | """ 103 | Used for the cancellation of an order via it's ID. This ID is provided 104 | when the user creates a new order. 105 | 106 | Args: 107 | order_id(str): Order must be not be filled 108 | 109 | Results: 110 | dict: These are the same fields returned by order/cancel 111 | example: { 112 | 'order_id': '86403510', 113 | 'id': '86403510', 114 | 'symbol': 'btcusd', 115 | 'exchange': 'gemini', 116 | 'avg_execution_price': '0.00', 117 | 'side': 'buy', 118 | 'type': 'exchange limit', 119 | 'timestamp': '1510403257', 120 | 'timestampms': 1510403257453, 121 | 'is_live': False, 122 | 'is_cancelled': True, 123 | 'is_hidden': False, 124 | 'was_forced': False, 125 | 'executed_amount': '0', 126 | 'remaining_amount': '0.02', 127 | 'options': ['maker-or-cancel'], 128 | 'price': '6400.28', 129 | 'original_amount': '0.02' 130 | } 131 | """ 132 | payload = { 133 | 'order_id': order_id 134 | } 135 | return self.api_query('/v1/order/cancel', payload) 136 | 137 | @typeassert(symbol=str, amount=str, side=str) 138 | def wrap_order(self, symbol, amount, side): 139 | """ 140 | This endpoint wraps and unwraps Gemini issued assets. 141 | 142 | Args: 143 | symbol(str): Only GUSDUSD 144 | amount(str): The amount of currency you want to buy/sell. 145 | side(str): Either "buy" or "sell" 146 | 147 | Results: 148 | dict: Returns the orderId 149 | "pair 150 | "price 151 | "priceCurrency 152 | "side 153 | "quantity 154 | "quantityCurrency, totalSpend, totalSpendCurrency, fee, feeCurrency, depositFee, depositFeeCurrency 155 | """ 156 | payload = { 157 | 'amount': amount, 158 | 'side': side, 159 | } 160 | return self.api_query('/v1/wrap/{}'.format(symbol), payload) 161 | 162 | def cancel_session_orders(self): 163 | """ 164 | Used for the cancellation of all orders in a session. 165 | 166 | Results: 167 | dict: The response will be a dict with two keys: "results" 168 | and "details" 169 | example: { 170 | 'result': 'ok', 171 | 'details': { 172 | 'cancelledOrders': [86403350, 86403386, 86403503, 86403612], 173 | 'cancelRejects': [] 174 | } 175 | } 176 | """ 177 | return self.api_query('/v1/order/cancel/session') 178 | 179 | def cancel_all_orders(self): 180 | """ 181 | Cancels all current orders open. 182 | 183 | Results: Same as cancel_session_order 184 | """ 185 | return self.api_query('/v1/order/cancel/all') 186 | 187 | # Order Status API 188 | @typeassert(order_id=str) 189 | def status_of_order(self, order_id): 190 | """ 191 | Get's the status of an order. 192 | Note: the API used to access this endpoint must have the "trader" 193 | functionality assigned to it. 194 | 195 | Args: 196 | order_id(str): Order can be in any state 197 | 198 | Results: 199 | dict: Returns the order_id, id, symbol, exchange, avh_execution_price, 200 | side, type, timestamp, timestampms, is_live, is_cancelled, is_hidden, 201 | was_forced, exucuted_amount, remaining_amount, options, price and 202 | original_amount 203 | example: { 204 | 'order_id': '44375901', 205 | 'id': '44375901', 206 | 'symbol': 'btcusd', 207 | 'exchange': 'gemini', 208 | 'avg_execution_price': '400.00', 209 | 'side': 'buy', 210 | 'type': 'exchange limit', 211 | 'timestamp': '1494870642', 212 | 'timestampms': 1494870642156, 213 | 'is_live': False, 214 | 'is_cancelled': False, 215 | 'is_hidden': False, 216 | 'was_forced': False, 217 | 'executed_amount': '3', 218 | 'remaining_amount': '0', 219 | 'options': [], 220 | 'price': '400.00', 221 | 'original_amount': '3' 222 | } 223 | """ 224 | payload = { 225 | 'order_id': order_id 226 | } 227 | return self.api_query('/v1/order/status', payload) 228 | 229 | def active_orders(self): 230 | """ 231 | Returns all the active_orders associated with the API. 232 | 233 | Results: 234 | array: An array of the results of /order/status for all your live orders. 235 | Each entry is similar to status_of_order 236 | """ 237 | return self.api_query('/v1/orders') 238 | 239 | @typeassert(symbol=str, limit_trades=int) 240 | def get_past_trades(self, symbol, limit_trades=None): 241 | """ 242 | Returns all the past trades associated with the API. 243 | Providing a limit_trade is optional. 244 | 245 | Args: 246 | symbols(str): Can be any value in self.symbols() 247 | limit_trades(int): Default value is 500 248 | 249 | Results: 250 | array: An array of of dicts of the past trades 251 | """ 252 | payload = { 253 | "symbol": symbol, 254 | "limit_trades": 500 if limit_trades is None else limit_trades 255 | } 256 | return self.api_query('/v1/mytrades', payload) 257 | 258 | def get_trade_volume(self): 259 | """ 260 | Returns the trade volume associated with the API for the past 261 | 30 days. 262 | 263 | Results: 264 | array: An array of dicts of the past trades 265 | """ 266 | return self.api_query('/v1/tradevolume') 267 | 268 | # Fund Management API 269 | def get_balance(self): 270 | """ 271 | This will show the available balances in the supported currencies. 272 | 273 | Results: 274 | array: An array of elements, with one block per currency 275 | example: [ 276 | { 277 | 'type': 'exchange', 278 | 'currency': 'BTC', 279 | 'amount': '19.17997442', 280 | 'available': '19.17997442', 281 | 'availableForWithdrawal': '19.17997442' 282 | }, 283 | { 284 | 'type': 'exchange', 285 | 'currency': 'USD', 286 | 'amount': '4831517.78', 287 | 'available': '4831389.45', 288 | 'availableForWithdrawal': '4831389.45' 289 | } 290 | ] 291 | """ 292 | return self.api_query('/v1/balances') 293 | 294 | @typeassert(currency=str, label=str) 295 | def create_deposit_address(self, currency, label=None): 296 | """ 297 | This will create a new cryptocurrency deposit address with an optional label. 298 | 299 | Args: 300 | currency(str): Can either be btc or eth 301 | label(str): Optional 302 | 303 | Results: 304 | dict: A dict of the following fields: currency, address, label 305 | """ 306 | if label: 307 | payload = { 308 | "label": label 309 | } 310 | else: 311 | payload = {} 312 | return self.api_query('/v1/deposit/{}/newAddress'.format(currency), payload) 313 | 314 | @typeassert(currency=str, address=str, amount=str) 315 | def withdraw_to_address(self, currency, address, amount): 316 | """ 317 | This will allow you to withdraw currency from the address 318 | provided. 319 | Note: Before you can withdraw cryptocurrency funds to a whitelisted 320 | address, you need three things: cryptocurrency address whitelists 321 | needs to be enabled for your account, the address you want to withdraw 322 | funds to needs to already be on that whitelist and an API key with the 323 | Fund Manager role added. 324 | 325 | Args: 326 | current(str): Can either be btc or eth 327 | address(str): The address you want the money to be sent to 328 | amount(str): Amount you want to transfer 329 | 330 | Results: 331 | dict: A dict of the following fields: destination, amount, txHash 332 | """ 333 | payload = { 334 | "address": address, 335 | "amount": amount 336 | } 337 | return self.api_query('/v1/withdraw/{}'.format(currency), payload) 338 | 339 | # Transfers API 340 | @typeassert(limit_transfers=int, show_completed_deposit_advances=bool) 341 | def get_past_transfers(self, limit_transfers=None, show_completed_deposit_advances=False): 342 | """ 343 | Returns all the past transfers associated with the API. 344 | Providing a limit_trade is optional. 345 | Whether to display completed deposit advances. False by default. Must be set True to activate. Defaults to False. 346 | 347 | Args: 348 | limit_trades(int): Default value is 500 349 | show_completed_deposit_advances(bool): Default value is False 350 | 351 | Results: 352 | array: An array of of dicts of the past transfers 353 | """ 354 | payload = { 355 | "limit_transfers": 500 if limit_transfers is None else limit_transfers, 356 | "show_completed_deposit_advances": show_completed_deposit_advances 357 | } 358 | return self.api_query('/v1/transfers', payload) 359 | 360 | # HeartBeat API 361 | def revive_hearbeat(self): 362 | """ 363 | Revive the heartbeat if 'heartbeat' is selected for the API. 364 | """ 365 | return self.api_query('/v1/heartbeat') 366 | -------------------------------------------------------------------------------- /gemini/public_client.py: -------------------------------------------------------------------------------- 1 | # public_client.py 2 | # Mohammad Usman 3 | # 4 | # A python wrapper for Gemini's public API 5 | 6 | from .cached import Cached 7 | from .debugly import typeassert 8 | import requests 9 | import time 10 | import datetime 11 | 12 | 13 | class PublicClient(metaclass=Cached): 14 | @typeassert(sandbox=bool) 15 | def __init__(self, sandbox=False): 16 | if sandbox: 17 | self.public_base_url = 'https://api.sandbox.gemini.com/v1' 18 | else: 19 | self.public_base_url = 'https://api.gemini.com/v1' 20 | 21 | def symbols(self): 22 | """ 23 | This endpoint retrieves all available symbols for trading. 24 | 25 | Returns: 26 | list: Will output an array of supported symbols 27 | example: ['btcusd', 'ethbtc', 'ethusd'] 28 | """ 29 | r = requests.get(self.public_base_url + '/symbols') 30 | return r.json() 31 | 32 | @typeassert(product_id=str) 33 | def symbol_details(self, product_id): 34 | """ 35 | This endpoint retrieves extra detail on supported symbols, such as 36 | minimum order size, tick size, quote increment and more. 37 | 38 | Args: 39 | product_id(str): Can be any value in self.symbols() 40 | 41 | Returns: 42 | dict:tick_size, quote_increment, min_order_size, status and wrap_enabled 43 | example: { 44 | "symbol":"BTCUSD", 45 | "base_currency":"BTC", 46 | "quote_currency":"USD", 47 | "tick_size":1e-08, 48 | "quote_increment":0.01, 49 | "min_order_size":"0.00001", 50 | "status":"open", 51 | "wrap_enabled":false 52 | } 53 | """ 54 | r = requests.get(self.public_base_url + '/symbols/details/' + product_id) 55 | return r.json() 56 | 57 | @typeassert(product_id=str) 58 | def get_ticker(self, product_id): 59 | """ 60 | This endpoint retrieves information about recent trading 61 | activity for the symbol. 62 | 63 | Args: 64 | product_id(str): Can be any value in self.symbols() 65 | 66 | Returns: 67 | dict: the latest bid, ask, last price qouted and the volume 68 | example: { 69 | 'bid': '6398.99', 70 | 'ask': '6399.00', 71 | 'volume': { 72 | 'BTC': '15122.8052525982', 73 | 'USD': '100216283.474911855175', 74 | 'timestamp': 1510407900000 75 | }, 76 | 'last': '6398.99' 77 | } 78 | """ 79 | r = requests.get(self.public_base_url + '/pubticker/' + product_id) 80 | return r.json() 81 | 82 | @typeassert(product_id=str) 83 | def get_current_order_book(self, product_id): 84 | """ 85 | This endpoint retreives information about the recents orders. 86 | 87 | Args: 88 | product_id(str): Can be any value in self.symbols() 89 | 90 | Returns: 91 | dict: This will return the current order book, as two arrays, 92 | one of bids, and one of asks 93 | example:{ 94 | 'bids': [ /* bids look like asks */ ], 95 | 'asks': [ 96 | { 97 | 'price': '6400.00', 98 | 'amount': '3.04177064', 99 | 'timestamp': '1510408074' 100 | }, 101 | ... 102 | ] 103 | } 104 | """ 105 | r = requests.get(self.public_base_url + '/book/' + product_id) 106 | return r.json() 107 | 108 | @typeassert(product_id=str, since=str) 109 | def get_trade_history(self, product_id, since=None): 110 | """ 111 | This endpoint will return the trades that have executed since the 112 | specified timestamp. Timestamps are either seconds or milliseconds 113 | since the epoch (1970-01-01). 114 | 115 | Args: 116 | product_id(str): Can be any value in self.symbols() 117 | since(str): Must be in DD/MM/YYYY format 118 | 119 | Returns: 120 | list: Will return at most 500 records 121 | example:[ 122 | { 123 | 'timestamp': 1510408136, 124 | 'timestampms': 1510408136595, 125 | 'tid': 2199657585, 126 | 'price': '6399.02', 127 | 'amount': '0.03906848', 128 | 'exchange': 'gemini', 129 | 'type': 'buy' 130 | }, 131 | ... 132 | ] 133 | """ 134 | if since is None: 135 | r = requests.get(self.public_base_url + '/trades/' + product_id) 136 | else: 137 | self.timestamp = time.mktime(datetime.datetime.strptime(since, 138 | "%d/%m/%Y").timetuple()) 139 | r = requests.get(self.public_base_url + '/trades/{}?since={}'.format( 140 | product_id, int(self.timestamp))) 141 | return r.json() 142 | 143 | @typeassert(product_id=str, since=str) 144 | def get_auction_history(self, product_id, since=None): 145 | """ 146 | This will return the auction events, optionally including 147 | publications of indicative prices, since the specific timestamp. 148 | 149 | Args: 150 | product_id(str): Can be any value in self.symbols() 151 | since(str): must be in DD/MM/YYYY format 152 | 153 | Returns: 154 | list: Will return at most 500 records if date is provided. 155 | Otherwise it'll output a dictionary for the current auction 156 | example:[ 157 | { 158 | 'last_auction_price': '6580.01', 159 | 'last_auction_quantity': '0.01515964', 160 | 'last_highest_bid_price': '6580.00', 161 | 'last_lowest_ask_price': '6580.01', 162 | 'next_update_ms': 1510433400000, 163 | 'next_auction_ms': 1510434000000, 164 | 'last_auction_eid': 2199289141 165 | }, 166 | ... 167 | ] 168 | """ 169 | if since is None: 170 | r = requests.get(self.public_base_url + '/auction/' + product_id) 171 | else: 172 | self.timestamp = time.mktime(datetime.datetime.strptime(since, 173 | "%d/%m/%Y").timetuple()) 174 | r = requests.get(self.public_base_url + '/auction/{}?since={}'.format( 175 | product_id, int(self.timestamp))) 176 | return r.json() 177 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtusman/gemini-python/a52da3d7eba64ebb342c969b751c1e52d83e2510/requirements.txt -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='gemini-python', 5 | version='0.2.1', 6 | packages=['gemini'], 7 | url='https://github.com/mtusman/gemini-python', 8 | license='MIT', 9 | author='Mohammad Usman', 10 | author_email='m.t.usman@hotmail.com', 11 | description='A python client for the Gemini API and Websocket', 12 | python_requires='>=3', 13 | install_requires=['requests', 'pytest', 'websocket', 'websocket-client'], 14 | classifiers=[ 15 | 'Development Status :: 5 - Production/Stable', 16 | 'Environment :: Console', 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 | 'Programming Language :: Python :: 3.6', 26 | ], 27 | keywords=['gemini', 'bitcoin', 'bitcoin-exchange', 'ethereum', 'ether', 'BTC', 'ETH', 'gemini-exchange'], 28 | ) 29 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtusman/gemini-python/a52da3d7eba64ebb342c969b751c1e52d83e2510/tests/__init__.py -------------------------------------------------------------------------------- /tests/keys.py: -------------------------------------------------------------------------------- 1 | public_key = '' 2 | private_key = '' 3 | -------------------------------------------------------------------------------- /tests/test_marketdataws.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.insert(0, '..') 4 | from gemini import MarketDataWS 5 | 6 | 7 | def client(): 8 | return MarketDataWS('btcusd', sandbox=True) 9 | 10 | 11 | class TestMarketDataWS: 12 | def test_on_message(self): 13 | r = client() 14 | r.on_message({'eventId': 2364280145, 15 | 'events': [{'delta': '-19.52358571', 16 | 'price': '9594.37', 17 | 'reason': 'cancel', 18 | 'remaining': '0', 19 | 'side': 'bid', 20 | 'type': 'change'}], 21 | 'socket_sequence': 0, 22 | 'timestamp': 1512076260, 23 | 'timestampms': 1512076260185, 24 | 'type': 'update'}) 25 | assert len(r.asks) == 0 26 | assert len(r.bids) == 0 27 | assert len(r.trades) == 0 28 | r.on_message({'eventId': 2364280145, 29 | 'events': [{'delta': '-19.52358571', 30 | 'price': '9594.37', 31 | 'reason': 'cancel', 32 | 'remaining': '0', 33 | 'side': 'bid', 34 | 'type': 'change'}], 35 | 'socket_sequence': 1, 36 | 'timestamp': 1512076260, 37 | 'timestampms': 1512076260185, 38 | 'type': 'update'}) 39 | assert len(r.asks) == 0 40 | assert len(r.bids) == 0 41 | assert len(r.trades) == 0 42 | r.on_message({'eventId': 2364281810, 43 | 'events': [{'amount': '0.3865', 44 | 'makerSide': 'ask', 45 | 'price': '9610.40', 46 | 'tid': 2364281810, 47 | 'type': 'trade'}, 48 | {'delta': '-0.3865', 49 | 'price': '9610.40', 50 | 'reason': 'trade', 51 | 'remaining': '1.7439', 52 | 'side': 'ask', 53 | 'type': 'change'}], 54 | 'socket_sequence': 884, 55 | 'timestamp': 1512076268, 56 | 'timestampms': 1512076268486, 57 | 'type': 'update'}) 58 | assert len(r.asks) == 1 59 | assert len(r.bids) == 0 60 | assert len(r.trades) == 1 61 | r.on_message({'eventId': 2364281810, 62 | 'events': [{'amount': '0.3865', 63 | 'makerSide': 'bid', 64 | 'price': '9610.40', 65 | 'tid': 2364281810, 66 | 'type': 'trade'}, 67 | {'delta': '-0.3865', 68 | 'price': '9610.40', 69 | 'reason': 'trade', 70 | 'remaining': '1.7439', 71 | 'side': 'ask', 72 | 'type': 'change'}], 73 | 'socket_sequence': 884, 74 | 'timestamp': 1512076268, 75 | 'timestampms': 1512076268486, 76 | 'type': 'update'}) 77 | assert len(r.asks) == 1 78 | assert len(r.bids) == 1 79 | assert len(r.trades) == 2 80 | 81 | def test_get_market_book(self): 82 | r = client() 83 | assert type(r.get_market_book()) is dict 84 | 85 | def test_reset_market_book(self): 86 | r = client() 87 | r.reset_market_book() 88 | assert len(r.asks) == 0 89 | assert len(r.bids) == 0 90 | 91 | def test_search_price(self): 92 | r = client() 93 | r.add('bid', {'eventId': 2364281810, 94 | 'events': [{'amount': '0.3865', 95 | 'makerSide': 'bid', 96 | 'price': '10000', 97 | 'tid': 2364281810, 98 | 'type': 'trade'}, 99 | {'delta': '-0.3865', 100 | 'price': '10000', 101 | 'reason': 'trade', 102 | 'remaining': '1.7439', 103 | 'side': 'bid', 104 | 'type': 'change'}], 105 | 'socket_sequence': 885, 106 | 'timestamp': 1512076268, 107 | 'timestampms': 1512076268486, 108 | 'type': 'update'}) 109 | result = r.search_price('10000') 110 | assert type(result) is dict 111 | assert "price" in result 112 | assert len(result["price"]) != 0 113 | 114 | def test_remove_from_bids(self): 115 | r = client() 116 | r.add('bid', {'eventId': 2364281810, 117 | 'events': [{'amount': '0.3865', 118 | 'makerSide': 'bid', 119 | 'price': '11000', 120 | 'tid': 2364281810, 121 | 'type': 'trade'}, 122 | {'delta': '-0.3865', 123 | 'price': '11000', 124 | 'reason': 'trade', 125 | 'remaining': '1.7439', 126 | 'side': 'bid', 127 | 'type': 'change'}], 128 | 'socket_sequence': 886, 129 | 'timestamp': 1512076268, 130 | 'timestampms': 1512076268486, 131 | 'type': 'update'}) 132 | search = r.search_price('11000') 133 | assert len(search['price']) == 1 134 | r.remove_from_bids('11000') 135 | search = r.search_price('11000') 136 | assert len(search['price']) == 0 137 | 138 | def test_remove_from_asks(self): 139 | r = client() 140 | r.add('ask', {'eventId': 2364281810, 141 | 'events': [{'amount': '0.3865', 142 | 'makerSide': 'ask', 143 | 'price': '12000', 144 | 'tid': 2364281810, 145 | 'type': 'trade'}, 146 | {'delta': '-0.3865', 147 | 'price': '12000', 148 | 'reason': 'trade', 149 | 'remaining': '1.7439', 150 | 'side': 'ask', 151 | 'type': 'change'}], 152 | 'socket_sequence': 886, 153 | 'timestamp': 1512076268, 154 | 'timestampms': 1512076268486, 155 | 'type': 'update'}) 156 | search = r.search_price('12000') 157 | assert len(search['price']) == 1 158 | r.remove_from_asks('12000') 159 | search = r.search_price('12000') 160 | assert len(search['price']) == 0 161 | 162 | def test_export_to_csv(self): 163 | r = client() 164 | r.export_to_csv(r'{}'.format(os.getcwd())) 165 | assert "gemini_market_data.csv" in os.listdir(r'{}'.format(os.getcwd())) 166 | os.remove("gemini_market_data.csv") 167 | 168 | def test_export_to_xml(self): 169 | r = client() 170 | r.export_to_xml(r'{}'.format(os.getcwd())) 171 | assert "gemini_market_data.xml" in os.listdir(r'{}'.format(os.getcwd())) 172 | os.remove("gemini_market_data.xml") 173 | -------------------------------------------------------------------------------- /tests/test_ordereventsws.py: -------------------------------------------------------------------------------- 1 | from .keys import public_key, private_key 2 | import sys 3 | import os 4 | import time 5 | sys.path.insert(0, '..') 6 | from gemini import OrderEventsWS 7 | 8 | 9 | def client(): 10 | return OrderEventsWS(public_key, private_key, sandbox=True) 11 | 12 | 13 | class TestOrderEventsWS: 14 | def test_reset_order_book(self): 15 | r = client() 16 | r._reset_order_book() 17 | for key in r.order_book.keys(): 18 | assert len(r.order_book[key]) == 0 19 | 20 | def test_api_query(self): 21 | r = client() 22 | r.start() 23 | time.sleep(5) 24 | assert len(r.order_book['subscription_ack']) != 0 25 | r.close() 26 | 27 | def test_on_message(self): 28 | r = client() 29 | for key in r.order_book.keys(): 30 | assert len(r.order_book[key]) == 0 31 | r.on_message({'accountId': 2117, 32 | 'apiSessionFilter': [], 33 | 'eventTypeFilter': [], 34 | 'subscriptionId': 'ws-order-events-2117-b01s1aqlv776oceke7t0', 35 | 'symbolFilter': [], 36 | 'type': 'subscription_ack'}) 37 | for key in r.order_book.keys(): 38 | if key == "subscription_ack": 39 | assert len(r.order_book[key]) == 1 40 | else: 41 | assert len(r.order_book[key]) == 0 42 | r.on_message({'sequence': 0, 43 | 'socket_sequence': 0, 44 | 'timestampms': 1512080326919, 45 | 'trace_id': 'b01s1aqlv776oceke7t0', 46 | 'type': 'heartbeat'}) 47 | for key in r.order_book.keys(): 48 | if key == "subscription_ack" or key == "heartbeat": 49 | assert len(r.order_book[key]) == 1 50 | else: 51 | assert len(r.order_book[key]) == 0 52 | for key in r.order_book.keys(): 53 | if key != "subscription_ack" and key != "heartbeat": 54 | r.on_message([{'api_session': 'lVTsC8CfoxkbkHVBKjEu', 55 | 'behavior': 'immediate-or-cancel', 56 | 'event_id': '86560107', 57 | 'is_cancelled': False, 58 | 'is_hidden': False, 59 | 'is_live': True, 60 | 'order_id': '86560106', 61 | 'order_type': 'exchange limit', 62 | 'original_amount': '0.1', 63 | 'price': '10000.00', 64 | 'side': 'buy', 65 | 'socket_sequence': 38, 66 | 'symbol': 'btcusd', 67 | 'timestamp': '1512080804', 68 | 'timestampms': 1512080804958, 69 | 'type': '{}'.format(key)}]) 70 | for key in r.order_book.keys(): 71 | assert len(r.order_book[key]) == 1 72 | 73 | def test_remove_order(self): 74 | r = client() 75 | r.on_message([{'api_session': 'lVTsC8CfoxkbkHVBKjEu', 76 | 'behavior': 'immediate-or-cancel', 77 | 'event_id': '86560107', 78 | 'is_cancelled': False, 79 | 'is_hidden': False, 80 | 'is_live': True, 81 | 'order_id': '86560106', 82 | 'order_type': 'exchange limit', 83 | 'original_amount': '0.1', 84 | 'price': '10000.00', 85 | 'side': 'buy', 86 | 'socket_sequence': 38, 87 | 'symbol': 'btcusd', 88 | 'timestamp': '1512080804', 89 | 'timestampms': 1512080804958, 90 | 'type': 'accepted'}]) 91 | assert len(r.order_book['accepted']) == 1 92 | r.remove_order('accepted', '86560106') 93 | assert len(r.order_book['accepted']) == 0 94 | 95 | def test_export_to_csv(self): 96 | r = client() 97 | r.on_message({'sequence': 0, 98 | 'socket_sequence': 0, 99 | 'timestampms': 1512080326919, 100 | 'trace_id': 'b01s1aqlv776oceke7t0', 101 | 'type': 'heartbeat'}) 102 | r.export_to_csv(r'{}'.format(os.getcwd()), 'heartbeat') 103 | assert "gemini_order_events.csv" in os.listdir(r'{}'.format(os.getcwd())) 104 | os.remove("gemini_order_events.csv") 105 | 106 | def test_export_to_xml(self): 107 | r = client() 108 | r.on_message({'sequence': 0, 109 | 'socket_sequence': 0, 110 | 'timestampms': 1512080326919, 111 | 'trace_id': 'b01s1aqlv776oceke7t0', 112 | 'type': 'heartbeat'}) 113 | r.export_to_xml(r'{}'.format(os.getcwd()), 'heartbeat') 114 | assert "gemini_order_events.xml" in os.listdir(r'{}'.format(os.getcwd())) 115 | os.remove("gemini_order_events.xml") 116 | -------------------------------------------------------------------------------- /tests/test_private_client.py: -------------------------------------------------------------------------------- 1 | from .keys import public_key, private_key 2 | import sys 3 | sys.path.insert(0, '..') 4 | from gemini import PrivateClient 5 | 6 | 7 | def client(): 8 | return PrivateClient(public_key, private_key, sandbox=True) 9 | 10 | 11 | class TestPrivateClient: 12 | def test_new_order(self): 13 | r = client() 14 | new_order = r.new_order("BTCUSD", "0.02", "6400.28", "buy", ["maker-or-cancel"]) 15 | assert type(new_order) is dict 16 | assert "order_id" in new_order 17 | assert "id" in new_order 18 | assert "symbol" in new_order 19 | assert "exchange" in new_order 20 | assert "avg_execution_price" in new_order 21 | assert "side" in new_order 22 | assert "type" in new_order 23 | assert "timestamp" in new_order 24 | assert "timestampms" in new_order 25 | assert "is_live" in new_order 26 | assert "is_cancelled" in new_order 27 | assert "is_hidden" in new_order 28 | assert "was_forced" in new_order 29 | assert "executed_amount" in new_order 30 | assert "remaining_amount" in new_order 31 | assert "options" in new_order 32 | assert "price" in new_order 33 | assert "original_amount" in new_order 34 | 35 | def test_cancel_order(self): 36 | r = client() 37 | new_order = r.new_order("BTCUSD", "0.02", "6400.28", "buy", ["maker-or-cancel"]) 38 | cancel_order = r.cancel_order(new_order["order_id"]) 39 | assert type(cancel_order) is dict 40 | assert "order_id" in cancel_order 41 | assert "id" in cancel_order 42 | assert "symbol" in cancel_order 43 | assert "exchange" in cancel_order 44 | assert "avg_execution_price" in cancel_order 45 | assert "side" in cancel_order 46 | assert "type" in cancel_order 47 | assert "timestamp" in cancel_order 48 | assert "timestampms" in cancel_order 49 | assert "is_live" in cancel_order 50 | assert "is_cancelled" in cancel_order 51 | assert "is_hidden" in cancel_order 52 | assert "was_forced" in cancel_order 53 | assert "executed_amount" in cancel_order 54 | assert "remaining_amount" in cancel_order 55 | assert "options" in cancel_order 56 | assert "price" in cancel_order 57 | assert "original_amount" in cancel_order 58 | 59 | def test_cancel_session_orders(self): 60 | r = client() 61 | new_order = r.new_order("BTCUSD", "0.02", "6400.28", "buy", ["maker-or-cancel"]) 62 | cancel_session_orders = r.cancel_session_orders() 63 | assert type(cancel_session_orders) is dict 64 | assert "result" in cancel_session_orders 65 | assert "details" in cancel_session_orders 66 | 67 | def test_cancel_orders(self): 68 | r = client() 69 | new_order = r.new_order("BTCUSD", "0.02", "6400.28", "buy", ["maker-or-cancel"]) 70 | cancel_all_orders = r.cancel_all_orders() 71 | assert type(cancel_all_orders) is dict 72 | assert "result" in cancel_all_orders 73 | assert "details" in cancel_all_orders 74 | 75 | def test_wrap_order(self): 76 | r = client() 77 | wrap_order = r.wrap_order("GUSDUSD", "10", "buy") 78 | assert type(wrap_order) is dict 79 | # Endpoint not supported on sandbox 80 | assert "error" in wrap_order #{'error': 'Encountered an error attempting to place a wrap/unwrap trade.'} 81 | # assert "orderId" in wrap_order 82 | # assert "pair" in wrap_order 83 | # assert "price" in wrap_order 84 | # assert "priceCurrency" in wrap_order 85 | # assert "side" in wrap_order 86 | # assert "quantity" in wrap_order 87 | # assert "quantityCurrency" in wrap_order 88 | # assert "totalSpend" in wrap_order 89 | # assert "totalSpendCurrency" in wrap_order 90 | # assert "fee" in wrap_order 91 | # assert "feeCurrency" in wrap_order 92 | # assert "depositFee" in wrap_order 93 | # assert "depositFeeCurrency" in wrap_order 94 | 95 | def test_status_of_orders(self): 96 | r = client() 97 | new_order = r.new_order("BTCUSD", "0.02", "6400.28", "buy", ["maker-or-cancel"]) 98 | status_of_order = r.status_of_order(new_order["order_id"]) 99 | assert type(status_of_order) is dict 100 | assert "order_id" in status_of_order 101 | assert "id" in status_of_order 102 | assert "symbol" in status_of_order 103 | assert "exchange" in status_of_order 104 | assert "avg_execution_price" in status_of_order 105 | assert "side" in status_of_order 106 | assert "type" in status_of_order 107 | assert "timestamp" in status_of_order 108 | assert "timestampms" in status_of_order 109 | assert "is_live" in status_of_order 110 | assert "is_cancelled" in status_of_order 111 | assert "is_hidden" in status_of_order 112 | assert "was_forced" in status_of_order 113 | assert "executed_amount" in status_of_order 114 | assert "remaining_amount" in status_of_order 115 | assert "options" in status_of_order 116 | assert "price" in status_of_order 117 | assert "original_amount" in status_of_order 118 | 119 | def test_active_orders(self): 120 | r = client() 121 | new_order = r.new_order("BTCUSD", "0.02", "6400.28", "buy", ["maker-or-cancel"]) 122 | active_orders = r.active_orders() 123 | assert type(active_orders) is list 124 | 125 | def test_get_past_trades(self): 126 | r = client() 127 | get_past_trades = r.get_past_trades("BTCUSD") 128 | assert type(get_past_trades) is list 129 | 130 | def test_get_trade_volume(self): 131 | r = client() 132 | get_trade_volume = r.get_trade_volume() 133 | assert type(get_trade_volume) is list 134 | 135 | def test_get_balance(self): 136 | r = client() 137 | get_past_trades = r.get_past_trades("BTCUSD") 138 | assert type(get_past_trades) is list 139 | 140 | def test_get_balance(self): 141 | r = client() 142 | get_balance = r.get_balance() 143 | assert type(get_balance) is list 144 | 145 | def test_create_deposit_address(self): 146 | r = client() 147 | create_deposit_address = r.create_deposit_address("ETH", label="testing") 148 | assert type(create_deposit_address) is dict 149 | 150 | def test_withdraw_to_address(self): 151 | r = PrivateClient("PUBLIC_KEY", "PRIVATE_CLIENT") 152 | withdraw_to_address = r.withdraw_to_address("ETH", "200", "0x0287b1B0032Dc42c16640F71BA06F1A87C3a7101") 153 | assert type(withdraw_to_address) is dict 154 | 155 | def test_revive_heartbeat(self): 156 | r = client() 157 | revive_hearbeat = r.revive_hearbeat() 158 | assert type(revive_hearbeat) is dict 159 | -------------------------------------------------------------------------------- /tests/test_public_client.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.insert(0, '..') 3 | from gemini import PublicClient 4 | 5 | 6 | def client(): 7 | return PublicClient(sandbox=True) 8 | 9 | 10 | class TestPublicClient: 11 | def test_get_ticker(self): 12 | r = client() 13 | ticker = r.get_ticker("BTCUSD") 14 | assert type(ticker) is dict 15 | assert "bid" in ticker 16 | assert "ask" in ticker 17 | assert "volume" in ticker 18 | assert "last" in ticker 19 | 20 | def test_get_current_order_book(self): 21 | r = client() 22 | order_book = r.get_current_order_book("BTCUSD") 23 | assert type(order_book) is dict 24 | assert "bid" or "ask" in order_book 25 | 26 | def test_get_trade_history(self): 27 | r = client() 28 | trade_history = r.get_trade_history("BTCUSD") 29 | assert type(trade_history) is list 30 | assert "timestamp" in trade_history[0] 31 | assert "timestampms" in trade_history[0] 32 | assert "tid" in trade_history[0] 33 | assert "price" in trade_history[0] 34 | assert "amount" in trade_history[0] 35 | assert "exchange" in trade_history[0] 36 | assert "type" in trade_history[0] 37 | 38 | def test_get_auction_history(self): 39 | r = client() 40 | auction_history = r.get_auction_history("BTCUSD") 41 | assert auction_history is list or dict 42 | 43 | def test_symbols(self): 44 | r = client() 45 | symbols = r.symbols() 46 | assert type(symbols) is list 47 | 48 | def test_symbol_details(self): 49 | r = client() 50 | symbol_details = r.symbol_details("BTCUSD") 51 | assert type(symbol_details) is dict 52 | assert "symbol" in symbol_details 53 | assert "base_currency" in symbol_details 54 | assert "quote_currency" in symbol_details 55 | assert "tick_size" in symbol_details 56 | assert "quote_increment" in symbol_details 57 | assert "min_order_size" in symbol_details 58 | assert "status" in symbol_details 59 | assert "wrap_enabled" in symbol_details 60 | --------------------------------------------------------------------------------