├── .gitignore ├── LICENSE ├── README.rst ├── bittrex_websocket ├── __init__.py ├── _abc.py ├── _auxiliary.py ├── _exceptions.py ├── _logger.py ├── _queue_events.py ├── _signalr.py ├── constants.py ├── order_book.py └── websocket_client.py ├── docker ├── .dockerignore ├── Dockerfile └── example.py ├── examples ├── order_book.py ├── record_trades.py └── ticker_updates.py ├── requirements.txt ├── resources ├── README_OLD.md ├── archieved_changelog.txt └── py_btrx.svg ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | 107 | # Other 108 | .idea/ 109 | test.py 110 | old_releases/ 111 | pypi_build.sh 112 | 113 | # End of https://www.gitignore.io/api/python 114 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 Stanislav Lazarov 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.rst: -------------------------------------------------------------------------------- 1 | |logo| bittrex-websocket 2 | ===================== 3 | 4 | |pypi-v2| |pypi-pyversions2| |pypi-l2| |pypi-wheel2| 5 | 6 | .. |pypi-v2| image:: https://img.shields.io/pypi/v/bittrex-websocket.svg 7 | :target: https://pypi.python.org/pypi/bittrex-websocket 8 | 9 | .. |pypi-pyversions2| image:: https://img.shields.io/pypi/pyversions/bittrex-websocket.svg 10 | :target: https://pypi.python.org/pypi/bittrex-websocket 11 | 12 | .. |pypi-l2| image:: https://img.shields.io/pypi/l/bittrex-websocket.svg 13 | :target: https://pypi.python.org/pypi/bittrex-websocket 14 | 15 | .. |pypi-wheel2| image:: https://img.shields.io/pypi/wheel/bittrex-websocket.svg 16 | :target: https://pypi.python.org/pypi/bittrex-websocket 17 | 18 | .. |logo| image:: /resources/py_btrx.svg 19 | :width: 60px 20 | 21 | What is ``bittrex-websocket``? 22 | -------------------------- 23 | Python Bittrex WebSocket (PBW) is the first unofficial Python wrapper for 24 | the `Bittrex Websocket API `_. 25 | It provides users with a simple and easy to use interface to the `Bittrex Exchange `_. 26 | 27 | Users can use it to access real-time public data (e.g exchange status, summary ticks and order fills) and account-level data such as order and balance status. The goal of the package is to serve as a foundation block which users can use to build upon their applications. Examples usages can include maintaining live order books, recording trade history, analysing order flow and many more. 28 | 29 | If you prefer ``asyncio``, then take a look at my other library: `bittrex-websocket-aio `_. 30 | 31 | -------------- 32 | 33 | Documentation 34 | http://python-bittrex-websocket-docs.readthedocs.io/en/latest/ 35 | 36 | Getting started/How-to 37 | http://python-bittrex-websocket-docs.readthedocs.io/en/latest/howto.html 38 | 39 | Methods 40 | http://python-bittrex-websocket-docs.readthedocs.io/en/latest/methods.html 41 | 42 | Changelog 43 | http://python-bittrex-websocket-docs.readthedocs.io/en/latest/changelog.html#bittrex-websocket 44 | 45 | I am constantly working on new features. Make sure you stay up to date by regularly checking the official docs! 46 | 47 | **Having an issue or a question? Found a bug or perhaps you want to contribute? Open an issue!** 48 | 49 | Quick Start 50 | ----------- 51 | .. code:: bash 52 | 53 | pip install bittrex-websocket 54 | 55 | .. code:: python 56 | 57 | #!/usr/bin/python 58 | # /examples/ticker_updates.py 59 | 60 | # Sample script showing how subscribe_to_exchange_deltas() works. 61 | 62 | # Overview: 63 | # --------- 64 | # 1) Creates a custom ticker_updates_container dict. 65 | # 2) Subscribes to N tickers and starts receiving market data. 66 | # 3) When information is received, checks if the ticker is 67 | # in ticker_updates_container and adds it if not. 68 | # 4) Disconnects when it has data information for each ticker. 69 | 70 | from bittrex_websocket.websocket_client import BittrexSocket 71 | from time import sleep 72 | 73 | def main(): 74 | class MySocket(BittrexSocket): 75 | 76 | def on_public(self, msg): 77 | name = msg['M'] 78 | if name not in ticker_updates_container: 79 | ticker_updates_container[name] = msg 80 | print('Just received market update for {}.'.format(name)) 81 | 82 | # Create container 83 | ticker_updates_container = {} 84 | # Create the socket instance 85 | ws = MySocket() 86 | # Enable logging 87 | ws.enable_log() 88 | # Define tickers 89 | tickers = ['BTC-ETH', 'BTC-NEO', 'BTC-ZEC', 'ETH-NEO', 'ETH-ZEC'] 90 | # Subscribe to ticker information 91 | for ticker in tickers: 92 | sleep(0.01) 93 | ws.subscribe_to_exchange_deltas([ticker]) 94 | 95 | # Users can also subscribe without introducing delays during invoking but 96 | # it is the recommended way when you are subscribing to a large list of tickers. 97 | # ws.subscribe_to_exchange_deltas(tickers) 98 | 99 | while len(ticker_updates_container) < len(tickers): 100 | sleep(1) 101 | else: 102 | print('We have received updates for all tickers. Closing...') 103 | ws.disconnect() 104 | sleep(10) 105 | 106 | if __name__ == "__main__": 107 | main() 108 | 109 | Order book syncing 110 | ------------------ 111 | 112 | .. code:: python 113 | 114 | #!/usr/bin/python 115 | # /examples/order_book.py 116 | 117 | # Sample script showing how order book syncing works. 118 | 119 | from __future__ import print_function 120 | from time import sleep 121 | from bittrex_websocket import OrderBook 122 | 123 | def main(): 124 | class MySocket(OrderBook): 125 | def on_ping(self, msg): 126 | print('Received order book update for {}'.format(msg)) 127 | 128 | # Create the socket instance 129 | ws = MySocket() 130 | # Enable logging 131 | ws.enable_log() 132 | # Define tickers 133 | tickers = ['BTC-ETH'] 134 | # Subscribe to order book updates 135 | ws.subscribe_to_orderbook(tickers) 136 | 137 | while True: 138 | sleep(10) 139 | book = ws.get_order_book('BTC-ETH') 140 | print(book[u'S'][0]) 141 | else: 142 | pass 143 | 144 | if __name__ == "__main__": 145 | main() 146 | 147 | 148 | Disclaimer 149 | ---------- 150 | I am not associated with Bittrex. Use the library at your own risk, I don't bear any responsibility if you end up losing your money. 151 | -------------------------------------------------------------------------------- /bittrex_websocket/__init__.py: -------------------------------------------------------------------------------- 1 | from bittrex_websocket import _logger 2 | from bittrex_websocket.websocket_client import BittrexSocket 3 | from bittrex_websocket.order_book import OrderBook 4 | from bittrex_websocket.constants import BittrexMethods 5 | -------------------------------------------------------------------------------- /bittrex_websocket/_abc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # bittrex_websocket/_abc.py 5 | # Stanislav Lazarov 6 | # 7 | # Official Bittrex documentation: https://github.com/Bittrex/beta 8 | 9 | from abc import ABCMeta, abstractmethod 10 | 11 | 12 | class WebSocket(object): 13 | __metaclass__ = ABCMeta 14 | 15 | @abstractmethod 16 | def subscribe_to_exchange_deltas(self, tickers): 17 | """ 18 | Allows the caller to receive real-time updates to the state of a SINGLE market. 19 | Upon subscribing, the callback will be invoked with market deltas as they occur. 20 | 21 | This feed only contains updates to exchange state. To form a complete picture of 22 | exchange state, users must first call QueryExchangeState and merge deltas into 23 | the data structure returned in that call. 24 | 25 | :param tickers: A list of tickers you are interested in. 26 | :type tickers: [] 27 | 28 | https://github.com/Bittrex/beta/#subscribetoexchangedeltas 29 | 30 | JSON Payload: 31 | { 32 | MarketName: string, 33 | Nonce: int, 34 | Buys: 35 | [ 36 | { 37 | Type: string - enum(ADD | REMOVE | UPDATE), 38 | Rate: decimal, 39 | Quantity: decimal 40 | } 41 | ], 42 | Sells: 43 | [ 44 | { 45 | Type: string - enum(ADD | REMOVE | UPDATE), 46 | Rate: decimal, 47 | Quantity: decimal 48 | } 49 | ], 50 | Fills: 51 | [ 52 | { 53 | OrderType: string, 54 | Rate: decimal, 55 | Quantity: decimal, 56 | TimeStamp: date 57 | } 58 | ] 59 | } 60 | """ 61 | 62 | @abstractmethod 63 | def subscribe_to_summary_deltas(self): 64 | """ 65 | Allows the caller to receive real-time updates of the state of ALL markets. 66 | Upon subscribing, the callback will be invoked with market deltas as they occur. 67 | 68 | Summary delta callbacks are verbose. A subset of the same data limited to the 69 | market name, the last price, and the base currency volume can be obtained via 70 | `subscribe_to_summary_lite_deltas`. 71 | 72 | https://github.com/Bittrex/beta#subscribetosummarydeltas 73 | 74 | JSON Payload: 75 | { 76 | Nonce : int, 77 | Deltas : 78 | [ 79 | { 80 | MarketName : string, 81 | High : decimal, 82 | Low : decimal, 83 | Volume : decimal, 84 | Last : decimal, 85 | BaseVolume : decimal, 86 | TimeStamp : date, 87 | Bid : decimal, 88 | Ask : decimal, 89 | OpenBuyOrders : int, 90 | OpenSellOrders : int, 91 | PrevDay : decimal, 92 | Created : date 93 | } 94 | ] 95 | } 96 | """ 97 | 98 | @abstractmethod 99 | def subscribe_to_summary_lite_deltas(self): 100 | """ 101 | Similar to `subscribe_to_summary_deltas`. 102 | Shows only market name, last price and base currency volume. 103 | 104 | JSON Payload: 105 | { 106 | Deltas: 107 | [ 108 | { 109 | MarketName: string, 110 | Last: decimal, 111 | BaseVolume: decimal 112 | } 113 | ] 114 | } 115 | """ 116 | 117 | @abstractmethod 118 | def query_summary_state(self): 119 | """ 120 | Allows the caller to retrieve the full state for all markets. 121 | 122 | JSON payload: 123 | { 124 | Nonce: int, 125 | Summaries: 126 | [ 127 | { 128 | MarketName: string, 129 | High: decimal, 130 | Low: decimal, 131 | Volume: decimal, 132 | Last: decimal, 133 | BaseVolume: decimal, 134 | TimeStamp: date, 135 | Bid: decimal, 136 | Ask: decimal, 137 | OpenBuyOrders: int, 138 | OpenSellOrders: int, 139 | PrevDay: decimal, 140 | Created: date 141 | } 142 | ] 143 | } 144 | """ 145 | 146 | @abstractmethod 147 | def query_exchange_state(self, tickers): 148 | """ 149 | Allows the caller to retrieve the full order book for a specific market. 150 | 151 | :param tickers: A list of tickers you are interested in. 152 | :type tickers: [] 153 | 154 | JSON payload: 155 | { 156 | MarketName : string, 157 | Nonce : int, 158 | Buys: 159 | [ 160 | { 161 | Quantity : decimal, 162 | Rate : decimal 163 | } 164 | ], 165 | Sells: 166 | [ 167 | { 168 | Quantity : decimal, 169 | Rate : decimal 170 | } 171 | ], 172 | Fills: 173 | [ 174 | { 175 | Id : int, 176 | TimeStamp : date, 177 | Quantity : decimal, 178 | Price : decimal, 179 | Total : decimal, 180 | FillType : string, 181 | OrderType : string 182 | } 183 | ] 184 | } 185 | """ 186 | 187 | @abstractmethod 188 | def authenticate(self, api_key, api_secret): 189 | """ 190 | Verifies a user’s identity to the server and begins receiving account-level notifications 191 | 192 | :param api_key: Your api_key with the relevant permissions. 193 | :type api_key: str 194 | :param api_secret: Your api_secret with the relevant permissions. 195 | :type api_secret: str 196 | 197 | https://github.com/slazarov/beta#authenticate 198 | """ 199 | 200 | @abstractmethod 201 | def on_public(self, msg): 202 | """ 203 | Streams all incoming messages from public delta channels. 204 | """ 205 | 206 | @abstractmethod 207 | def on_private(self, msg): 208 | """ 209 | Streams all incoming messages from private delta channels. 210 | """ 211 | -------------------------------------------------------------------------------- /bittrex_websocket/_auxiliary.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # bittrex_websocket/_auxiliary.py 5 | # Stanislav Lazarov 6 | 7 | import logging 8 | from uuid import uuid4 9 | import hashlib 10 | import hmac 11 | 12 | try: 13 | from pybase64 import b64decode, b64encode 14 | except: 15 | from base64 import b64decode, b64encode 16 | from zlib import decompress, MAX_WBITS 17 | 18 | try: 19 | from ujson import loads 20 | except: 21 | from json import loads 22 | 23 | logger = logging.getLogger(__name__) 24 | 25 | 26 | def process_message(message): 27 | try: 28 | deflated_msg = decompress(b64decode(message, validate=True), -MAX_WBITS) 29 | except SyntaxError as e: 30 | deflated_msg = decompress(b64decode(message, validate=True)) 31 | except TypeError: 32 | # Python2 doesn't have validate 33 | deflated_msg = decompress(b64decode(message), -MAX_WBITS) 34 | return loads(deflated_msg.decode()) 35 | 36 | 37 | def create_signature(api_secret, challenge): 38 | api_sign = hmac.new(api_secret.encode(), challenge.encode(), hashlib.sha512).hexdigest() 39 | return api_sign 40 | 41 | 42 | def clear_queue(q): 43 | q.mutex.acquire() 44 | q.queue.clear() 45 | q.all_tasks_done.notify_all() 46 | q.unfinished_tasks = 1 47 | q.mutex.release() 48 | 49 | 50 | def identify_payload(payload): 51 | key = payload[0][0] if type(payload[0]) == list else payload[0] 52 | return key 53 | 54 | 55 | class BittrexConnection(object): 56 | def __init__(self, conn, hub): 57 | self.conn = conn 58 | self.corehub = hub 59 | self.id = uuid4().hex 60 | -------------------------------------------------------------------------------- /bittrex_websocket/_exceptions.py: -------------------------------------------------------------------------------- 1 | from requests.exceptions import HTTPError, MissingSchema, ConnectionError 2 | from urllib3.contrib.pyopenssl import SocketError 3 | from urllib3.exceptions import TimeoutError as TimeoutErrorUrlLib 4 | from websocket import WebSocketConnectionClosedException, WebSocketBadStatusException, WebSocketTimeoutException 5 | 6 | try: 7 | class TimeoutError(TimeoutError): 8 | def __init__(self): 9 | super(TimeoutError, self).__init__() 10 | 11 | except NameError: 12 | class TimeoutError(Exception): 13 | def __init__(self): 14 | super(TimeoutError, self).__init__() 15 | 16 | 17 | class WebSocketConnectionClosedByUser(Exception): 18 | pass 19 | -------------------------------------------------------------------------------- /bittrex_websocket/_logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | try: # Python 2.7+ 4 | from logging import NullHandler 5 | except ImportError: 6 | class NullHandler(logging.Handler): 7 | def emit(self, record): 8 | pass 9 | 10 | logging.getLogger(__package__).addHandler(NullHandler()) 11 | 12 | 13 | def add_stream_logger(level=logging.DEBUG, file_name=None): 14 | logger = logging.getLogger(__package__) 15 | logger.setLevel(level) 16 | if file_name is not None: 17 | logger.addHandler(_get_file_handler(file_name)) 18 | logger.addHandler(_get_stream_handler()) 19 | 20 | 21 | def remove_stream_logger(): 22 | logger = logging.getLogger(__package__) 23 | logger.propagate = False 24 | for handler in list(logger.handlers): 25 | logger.removeHandler(handler) 26 | logger.addHandler(NullHandler()) 27 | 28 | 29 | def _get_stream_handler(level=logging.DEBUG): 30 | handler = logging.StreamHandler() 31 | handler.setLevel(level) 32 | handler.setFormatter(_get_formatter()) 33 | return handler 34 | 35 | 36 | def _get_file_handler(file_name): 37 | file_handler = logging.FileHandler(file_name) 38 | file_handler.setFormatter(_get_formatter()) 39 | return file_handler 40 | 41 | 42 | def _get_formatter(): 43 | msg_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' 44 | date_format = '%Y-%m-%d %H:%M:%S' 45 | formatter = logging.Formatter(msg_format, date_format) 46 | return formatter 47 | -------------------------------------------------------------------------------- /bittrex_websocket/_queue_events.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # bittrex_websocket/_queue_events.py 5 | # Stanislav Lazarov 6 | 7 | from .constants import EventTypes 8 | 9 | 10 | class Event(object): 11 | """ 12 | Event is base class providing an interface 13 | for all subsequent(inherited) events. 14 | """ 15 | 16 | 17 | class ConnectEvent(Event): 18 | """ 19 | Handles the event of creating a new connection. 20 | """ 21 | 22 | def __init__(self): 23 | self.type = EventTypes.CONNECT 24 | 25 | 26 | class SubscribeEvent(Event): 27 | """ 28 | Handles the event of subscribing specific ticker(s) to specific channels. 29 | """ 30 | 31 | def __init__(self, invoke, *payload): 32 | self.type = EventTypes.SUBSCRIBE 33 | self.invoke = invoke 34 | self.payload = payload 35 | 36 | 37 | class ReconnectEvent(Event): 38 | """ 39 | Handles the event reconnection. 40 | """ 41 | 42 | def __init__(self, error_message): 43 | self.type = EventTypes.RECONNECT 44 | self.error_message = error_message 45 | 46 | 47 | class CloseEvent(Event): 48 | """ 49 | Handles the event of closing the socket. 50 | """ 51 | 52 | def __init__(self): 53 | self.type = EventTypes.CLOSE 54 | 55 | 56 | class ConfirmEvent(Event): 57 | """ 58 | Handles the event of confirm the order book. 59 | """ 60 | 61 | def __init__(self, ticker, order_nounces): 62 | self.type = EventTypes.CONFIRM_OB 63 | self.ticker = ticker 64 | self.order_nounces = order_nounces 65 | 66 | 67 | class SyncEvent(Event): 68 | """ 69 | Handles the event of syncing the order book. 70 | """ 71 | 72 | def __init__(self, ticker, order_nounces): 73 | self.type = EventTypes.SYNC_OB 74 | self.ticker = ticker 75 | self.order_nounces = order_nounces 76 | -------------------------------------------------------------------------------- /bittrex_websocket/_signalr.py: -------------------------------------------------------------------------------- 1 | import signalr 2 | from threading import Thread 3 | from ._exceptions import WebSocketConnectionClosedByUser 4 | from .constants import OtherConstants 5 | 6 | import logging 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | try: 11 | from Queue import Queue 12 | except ImportError: 13 | from queue import Queue 14 | 15 | try: 16 | from ujson import dumps, loads 17 | except: 18 | from json import dumps, loads 19 | 20 | 21 | class Connection(signalr.Connection, object): 22 | def __init__(self, url, session): 23 | super(Connection, self).__init__(url, session) 24 | self.__transport = WebSocketsTransport(session, self) 25 | self.exception = None 26 | self.queue = Queue() 27 | self.__queue_handler = None 28 | 29 | def start(self): 30 | self.starting.fire() 31 | 32 | negotiate_data = self.__transport.negotiate() 33 | self.token = negotiate_data['ConnectionToken'] 34 | 35 | listener = self.__transport.start() 36 | 37 | def wrapped_listener(): 38 | self.is_open = True 39 | while self.is_open: 40 | try: 41 | listener() 42 | except Exception as e: 43 | event = QueueEvent(event_type='ERROR', payload=e) 44 | self.queue.put(event) 45 | self.is_open = False 46 | self.exception = e 47 | else: 48 | self.started = False 49 | 50 | self.__listener_thread = Thread(target=wrapped_listener, name=OtherConstants.SIGNALR_LISTENER_THREAD) 51 | self.__listener_thread.daemon = True 52 | self.__listener_thread.start() 53 | self.queue_handler() 54 | 55 | def queue_handler(self): 56 | while True: 57 | event = self.queue.get() 58 | try: 59 | if event is not None: 60 | if event.type == 'SEND': 61 | self.__transport.send(event.payload) 62 | elif event.type == 'ERROR': 63 | self.exit_gracefully() 64 | raise self.exception 65 | elif event.type == 'CLOSE': 66 | self.exit_gracefully() 67 | raise WebSocketConnectionClosedByUser('Connection closed by user.') 68 | elif event.type == 'FORCE_CLOSE': 69 | break 70 | finally: 71 | self.queue.task_done() 72 | 73 | def send(self, data): 74 | event = QueueEvent(event_type='SEND', payload=data) 75 | self.queue.put(event) 76 | 77 | def close(self): 78 | self.exit_gracefully() 79 | event = QueueEvent(event_type='CLOSE', payload=None) 80 | self.queue.put(event) 81 | 82 | def force_close(self): 83 | event = QueueEvent(event_type='FORCE_CLOSE', payload=None) 84 | self.queue.put(event) 85 | 86 | def exit_gracefully(self): 87 | self.is_open = False 88 | self.__transport.close() 89 | self.__listener_thread.join() 90 | 91 | 92 | ################# 93 | # UJSON support # 94 | ################# 95 | 96 | class WebSocketsTransport(signalr.transports._ws_transport.WebSocketsTransport, object): 97 | def __init__(self, session, connection): 98 | super(WebSocketsTransport, self).__init__(session, connection) 99 | self.ws = None 100 | self.__requests = {} 101 | 102 | def _handle_notification(self, message): 103 | if len(message) > 0: 104 | data = loads(message) 105 | self._connection.received.fire(**data) 106 | 107 | def send(self, data): 108 | self.ws.send(dumps(data)) 109 | 110 | 111 | ################ 112 | # Queue Events # 113 | ################ 114 | 115 | class QueueEvent(object): 116 | def __init__(self, event_type, payload): 117 | self.type = event_type 118 | self.payload = payload 119 | 120 | 121 | class ErrorEvent(object): 122 | def __init__(self, code, message): 123 | self.code = code 124 | self.message = message 125 | -------------------------------------------------------------------------------- /bittrex_websocket/constants.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # bittrex_websocket/constants.py 5 | # Stanislav Lazarov 6 | 7 | 8 | class Constant(object): 9 | """ 10 | Event is base class providing an interface 11 | for all subsequent(inherited) constants. 12 | """ 13 | 14 | 15 | class EventTypes(Constant): 16 | CONNECT = 'CONNECT' 17 | SUBSCRIBE = 'SUBSCRIBE' 18 | CLOSE = 'CLOSE' 19 | RECONNECT = 'RECONNECT' 20 | CONFIRM_OB = 'CONFIRM_OB' 21 | SYNC_OB = 'SYNC_OB' 22 | 23 | 24 | class BittrexParameters(Constant): 25 | # Connection parameters 26 | URL = 'https://socket.bittrex.com/signalr' 27 | HUB = 'c2' 28 | # Callbacks 29 | MARKET_DELTA = 'uE' 30 | SUMMARY_DELTA = 'uS' 31 | SUMMARY_DELTA_LITE = 'uL' 32 | BALANCE_DELTA = 'uB' 33 | ORDER_DELTA = 'uO' 34 | # Retry 35 | CONNECTION_TIMEOUT = 10 36 | RETRY_TIMEOUT = 5 37 | MAX_RETRIES = None 38 | 39 | 40 | class BittrexMethods(Constant): 41 | # Public methods 42 | SUBSCRIBE_TO_EXCHANGE_DELTAS = 'SubscribeToExchangeDeltas' 43 | SUBSCRIBE_TO_SUMMARY_DELTAS = 'SubscribeToSummaryDeltas' 44 | SUBSCRIBE_TO_SUMMARY_LITE_DELTAS = 'SubscribeToSummaryLiteDeltas' 45 | QUERY_SUMMARY_STATE = 'QuerySummaryState' 46 | QUERY_EXCHANGE_STATE = 'QueryExchangeState' 47 | GET_AUTH_CONTENT = 'GetAuthContext' 48 | AUTHENTICATE = 'Authenticate' 49 | 50 | 51 | class ErrorMessages(Constant): 52 | INVALID_TICKER_INPUT = 'Tickers must be submitted as a list.' 53 | UNHANDLED_EXCEPTION = '\nUnhandled {}.' \ 54 | '\nAuto-reconnection is disabled for unhandled exceptions.' \ 55 | '\nReport to https://github.com/slazarov/python-bittrex-websocket.' 56 | CONNECTION_TIMEOUTED = 'Connection timeout after {} seconds. Sending a reconnection signal.' 57 | 58 | 59 | class InfoMessages(Constant): 60 | SUCCESSFUL_DISCONNECT = 'Bittrex connection successfully closed.' 61 | RECONNECTION_COUNT_FINITE = 'Previous reconnection failed. Retrying in {} seconds. Reconnection attempt {} out of {}.' 62 | RECONNECTION_COUNT_INFINITE = 'Previous reconnection failed. Retrying in {} seconds. Reconnection attempt {}.' 63 | 64 | 65 | class OtherConstants(Constant): 66 | CF_SESSION_TYPE = '' 67 | SOCKET_CONNECTION_THREAD = 'SocketConnectionThread' 68 | SIGNALR_LISTENER_THREAD = 'SignalRListenerThread' 69 | -------------------------------------------------------------------------------- /bittrex_websocket/order_book.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # bittrex_websocket/order_book.py 5 | # Stanislav Lazarov 6 | 7 | from bittrex_websocket.websocket_client import BittrexSocket 8 | from bittrex_websocket.constants import BittrexMethods 9 | from ._queue_events import ConfirmEvent, SyncEvent 10 | import logging 11 | from .constants import EventTypes 12 | from collections import deque 13 | from time import sleep, time 14 | from events import Events 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | class OrderBook(BittrexSocket): 20 | def __init__(self, url=None, retry_timeout=None, max_retries=None): 21 | super(OrderBook, self).__init__(url, retry_timeout, max_retries) 22 | self._on_ping = Events() 23 | self._on_ping.on_change += self.on_ping 24 | self.order_nounces = {} 25 | self.order_books = {} 26 | 27 | def get_order_book(self, ticker): 28 | if ticker in self.order_books: 29 | return self.order_books[ticker] 30 | 31 | def control_queue_handler(self): 32 | while True: 33 | event = self.control_queue.get() 34 | if event is not None: 35 | if event.type == EventTypes.CONNECT: 36 | self._handle_connect() 37 | elif event.type == EventTypes.SUBSCRIBE: 38 | self._handle_subscribe(event) 39 | elif event.type == EventTypes.RECONNECT: 40 | self._handle_reconnect(event.error_message) 41 | elif event.type == EventTypes.CONFIRM_OB: 42 | self._confirm_order_book(event.ticker, event.order_nounces) 43 | elif event.type == EventTypes.SYNC_OB: 44 | self._sync_order_book(event.ticker, event.order_nounces) 45 | elif event.type == EventTypes.CLOSE: 46 | self.connection.conn.close() 47 | break 48 | self.control_queue.task_done() 49 | 50 | def subscribe_to_orderbook(self, ticker): 51 | self.subscribe_to_exchange_deltas(ticker) 52 | 53 | def _handle_subscribe_for_ticker(self, invoke, payload): 54 | if invoke in [BittrexMethods.SUBSCRIBE_TO_EXCHANGE_DELTAS, BittrexMethods.QUERY_EXCHANGE_STATE]: 55 | for ticker in payload[0]: 56 | self.connection.corehub.server.invoke(invoke, ticker) 57 | self.invokes.append({'invoke': invoke, 'ticker': ticker}) 58 | logger.info('Successfully subscribed to [{}] for [{}].'.format(invoke, ticker)) 59 | return True 60 | elif invoke is None: 61 | return True 62 | return False 63 | 64 | def on_public(self, msg): 65 | if msg['invoke_type'] == BittrexMethods.SUBSCRIBE_TO_EXCHANGE_DELTAS: 66 | ticker = msg['M'] 67 | if ticker not in self.order_nounces: 68 | self.order_nounces[ticker] = deque() 69 | self.query_exchange_state([ticker]) 70 | elif ticker in self.order_nounces: 71 | if self.order_nounces[ticker] is False: 72 | pass 73 | else: 74 | self.order_nounces[ticker].append(msg) 75 | if ticker in self.order_books: 76 | if self.order_nounces[ticker]: 77 | # event = ConfirmEvent(ticker, self.order_nounces[ticker]) 78 | self._confirm_order_book(ticker, self.order_nounces[ticker]) 79 | self.order_nounces[ticker] = False 80 | # self.control_queue.put(event) 81 | else: 82 | # event = SyncEvent(ticker, msg) 83 | # self.control_queue.put(event) 84 | self._sync_order_book(ticker, msg) 85 | elif msg['invoke_type'] == BittrexMethods.QUERY_EXCHANGE_STATE: 86 | ticker = msg['ticker'] 87 | self.order_books[ticker] = msg 88 | for i in range(len(self.invokes)): 89 | if self.invokes[i]['ticker'] == ticker and self.invokes[i]['invoke'] == msg['invoke_type']: 90 | self.invokes[i]['invoke'] = None 91 | break 92 | 93 | def _confirm_order_book(self, ticker, order_nounces): 94 | for delta in order_nounces: 95 | if self._sync_order_book(ticker, delta): 96 | return 97 | 98 | def _sync_order_book(self, ticker, order_data): 99 | # Syncs the order book for the pair, given the most recent data from the socket 100 | nounce_diff = order_data['N'] - self.order_books[ticker]['N'] 101 | if nounce_diff == 1: 102 | self.order_books[ticker]['N'] = order_data['N'] 103 | # Start syncing 104 | for side in [['Z', True], ['S', False]]: 105 | made_change = False 106 | for item in order_data[side[0]]: 107 | # TYPE 0: New order entries at matching price 108 | # -> ADD to order book 109 | if item['TY'] == 0: 110 | self.order_books[ticker][side[0]].append( 111 | { 112 | 'Q': item['Q'], 113 | 'R': item['R'] 114 | }) 115 | made_change = True 116 | 117 | # TYPE 1: Cancelled / filled order entries at matching price 118 | # -> DELETE from the order book 119 | elif item['TY'] == 1: 120 | for i, existing_order in enumerate( 121 | self.order_books[ticker][side[0]]): 122 | if existing_order['R'] == item['R']: 123 | del self.order_books[ticker][side[0]][i] 124 | # made_change = True 125 | break 126 | 127 | # TYPE 2: Changed order entries at matching price (partial fills, cancellations) 128 | # -> EDIT the order book 129 | elif item['TY'] == 2: 130 | for existing_order in self.order_books[ticker][side[0]]: 131 | if existing_order['R'] == item['R']: 132 | existing_order['Q'] = item['Q'] 133 | # made_change = True 134 | break 135 | 136 | if made_change: 137 | # Sort by price, with respect to BUY(desc) or SELL(asc) 138 | self.order_books[ticker][side[0]] = sorted( 139 | self.order_books[ticker][side[0]], key=lambda k: k['R'], reverse=side[1]) 140 | # Add nounce unix timestamp 141 | self.order_books[ticker]['timestamp'] = time() 142 | self._on_ping.on_change(ticker) 143 | return True 144 | # The next nounce will trigger a sync. 145 | elif nounce_diff == 0: 146 | return True 147 | # The order book snapshot nounce is ahead. Discard this nounce. 148 | elif nounce_diff < 0: 149 | return False 150 | else: 151 | # Experimental resync 152 | logger.error( 153 | '[Subscription][{}][{}]: Out of sync. Trying to resync...'.format(BittrexMethods.QUERY_EXCHANGE_STATE, 154 | ticker)) 155 | self.order_nounces.pop(ticker) 156 | self.order_books.pop(ticker) 157 | 158 | def on_ping(self, msg): 159 | pass 160 | -------------------------------------------------------------------------------- /bittrex_websocket/websocket_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # bittrex_websocket/websocket_client.py 5 | # Stanislav Lazarov 6 | 7 | from ._signalr import Connection 8 | import logging 9 | from ._logger import add_stream_logger, remove_stream_logger 10 | from threading import Thread 11 | from ._queue_events import * 12 | from .constants import EventTypes, BittrexParameters, BittrexMethods, ErrorMessages, InfoMessages, OtherConstants 13 | from ._auxiliary import process_message, create_signature, clear_queue, identify_payload, BittrexConnection 14 | from ._abc import WebSocket 15 | 16 | try: 17 | from cfscrape import create_scraper as Session 18 | except ImportError: 19 | from requests import Session 20 | from time import sleep, time 21 | 22 | try: 23 | from Queue import Queue 24 | except ImportError: 25 | from queue import Queue 26 | from events import Events 27 | from ._exceptions import * 28 | 29 | logger = logging.getLogger(__name__) 30 | 31 | 32 | class BittrexSocket(WebSocket): 33 | 34 | def __init__(self, url=None, retry_timeout=None, max_retries=None): 35 | """ 36 | :param url: Custom connection url 37 | :type url: str or None 38 | :param retry_timeout: Seconds between connection retries (DEFAULT = 10) 39 | :type retry_timeout: int 40 | :param max_retries: Maximum retries before quiting 41 | :type max_retries: int 42 | """ 43 | self.url = BittrexParameters.URL if url is None else url 44 | self.retry_timeout = BittrexParameters.RETRY_TIMEOUT if retry_timeout is None else retry_timeout 45 | self.max_retries = BittrexParameters.MAX_RETRIES if max_retries is None else max_retries 46 | self.retry_fail = 0 47 | self.last_retry = None 48 | self.control_queue = None 49 | self.invokes = [] 50 | self.tickers = None 51 | self.connection = None 52 | self.threads = [] 53 | self.credentials = None 54 | self._on_public_callback = None 55 | self._on_private_callback = None 56 | self._assign_callbacks() 57 | self._start_main_thread() 58 | 59 | def _assign_callbacks(self): 60 | self._on_public_callback = Events() 61 | self._on_public_callback.on_change += self.on_public 62 | self._on_private_callback = Events() 63 | self._on_private_callback.on_change += self.on_private 64 | 65 | def _start_main_thread(self): 66 | self.control_queue = Queue() 67 | self.control_queue.put(ConnectEvent()) 68 | thread = Thread(target=self.control_queue_handler, name='ControlQueueThread') 69 | thread.daemon = True 70 | self.threads.append(thread) 71 | thread.start() 72 | 73 | def control_queue_handler(self): 74 | while True: 75 | event = self.control_queue.get() 76 | if event is not None: 77 | if event.type == EventTypes.CONNECT: 78 | self._handle_connect() 79 | elif event.type == EventTypes.SUBSCRIBE: 80 | self._handle_subscribe(event) 81 | elif event.type == EventTypes.RECONNECT: 82 | self._handle_reconnect(event.error_message) 83 | elif event.type == EventTypes.CLOSE: 84 | self.connection.conn.close() 85 | break 86 | self.control_queue.task_done() 87 | 88 | def _handle_connect(self): 89 | if self.last_retry is not None and time() - self.last_retry >= 60: 90 | logger.debug('Last reconnection was more than 60 seconds ago. Resetting retry counter.') 91 | self.retry_fail = 0 92 | else: 93 | self.last_retry = time() 94 | connection = Connection(self.url, Session()) 95 | hub = connection.register_hub(BittrexParameters.HUB) 96 | connection.received += self._on_debug 97 | connection.error += self.on_error 98 | hub.client.on(BittrexParameters.MARKET_DELTA, self._on_public) 99 | hub.client.on(BittrexParameters.SUMMARY_DELTA, self._on_public) 100 | hub.client.on(BittrexParameters.SUMMARY_DELTA_LITE, self._on_public) 101 | hub.client.on(BittrexParameters.BALANCE_DELTA, self._on_private) 102 | hub.client.on(BittrexParameters.ORDER_DELTA, self._on_private) 103 | self.connection = BittrexConnection(connection, hub) 104 | thread = Thread(target=self._connection_handler, name=OtherConstants.SOCKET_CONNECTION_THREAD) 105 | thread.daemon = True 106 | self.threads.append(thread) 107 | thread.start() 108 | 109 | def _handle_reconnect(self, error_message): 110 | if error_message is not None: 111 | logger.error('{}.'.format(error_message)) 112 | logger.debug('Initiating reconnection procedure.') 113 | for i, thread in enumerate(self.threads): 114 | if thread.name == OtherConstants.SOCKET_CONNECTION_THREAD: 115 | thread.join() 116 | self.threads.pop(i) 117 | events = [] 118 | for item in self.invokes: 119 | event = SubscribeEvent(item['invoke'], [item['ticker']]) 120 | events.append(event) 121 | # Reset previous connection 122 | self.invokes, self.connection = [], None 123 | # Restart 124 | if 0 <= self.retry_fail <= self.retry_fail + 1 if self.max_retries is None else self.max_retries: 125 | # Don't delay the first reconnection. 126 | if BittrexParameters.MAX_RETRIES is not None: 127 | logger.debug(InfoMessages.RECONNECTION_COUNT_FINITE.format(self.retry_timeout, self.retry_fail + 1, 128 | BittrexParameters.MAX_RETRIES)) 129 | else: 130 | logger.debug(InfoMessages.RECONNECTION_COUNT_INFINITE.format(self.retry_timeout, self.retry_fail + 1)) 131 | if self.retry_fail > 0: 132 | sleep(self.retry_timeout) 133 | self.retry_fail += 1 134 | self.control_queue.put(ConnectEvent()) 135 | for event in events: 136 | self.control_queue.put(event) 137 | else: 138 | logger.debug('Maximum reconnection retries reached. Closing the socket instance.') 139 | self.control_queue.put(CloseEvent()) 140 | 141 | def _connection_handler(self): 142 | def _get_err_msg(exception): 143 | error_message = 'Exception = {}, Message = <{}>'.format(type(exception), exception) 144 | return error_message 145 | 146 | if str(type(Session())) == OtherConstants.CF_SESSION_TYPE: 147 | logger.info('Establishing connection to Bittrex through {}.'.format(self.url)) 148 | logger.info('cfscrape detected, will try to bypass Cloudflare if enabled.') 149 | else: 150 | logger.info('Establishing connection to Bittrex through {}.'.format(self.url)) 151 | try: 152 | self.connection.conn.start() 153 | except TimeoutError as e: 154 | self.control_queue.put(ReconnectEvent(_get_err_msg(e))) 155 | except WebSocketConnectionClosedByUser: 156 | logger.info(InfoMessages.SUCCESSFUL_DISCONNECT) 157 | except WebSocketConnectionClosedException as e: 158 | self.control_queue.put(ReconnectEvent(_get_err_msg(e))) 159 | except TimeoutErrorUrlLib as e: 160 | self.control_queue.put(ReconnectEvent(_get_err_msg(e))) 161 | except WebSocketTimeoutException as e: 162 | self.control_queue.put(ReconnectEvent(_get_err_msg(e))) 163 | except ConnectionError: 164 | pass 165 | # Commenting it for the time being. It should be handled in _handle_subscribe. 166 | # event = ReconnectEvent(None) 167 | # self.control_queue.put(event) 168 | except Exception as e: 169 | logger.error(ErrorMessages.UNHANDLED_EXCEPTION.format(_get_err_msg(e))) 170 | self.disconnect() 171 | # event = ReconnectEvent(None) 172 | # self.control_queue.put(event) 173 | 174 | def _handle_subscribe(self, sub_event): 175 | invoke, payload = sub_event.invoke, sub_event.payload 176 | i = 0 177 | while self.connection.conn.started is False: 178 | sleep(1) 179 | i += 1 180 | if i == BittrexParameters.CONNECTION_TIMEOUT: 181 | logger.error(ErrorMessages.CONNECTION_TIMEOUTED.format(BittrexParameters.CONNECTION_TIMEOUT)) 182 | self.invokes.append({'invoke': invoke, 'ticker': identify_payload(payload)}) 183 | for event in self.control_queue.queue: 184 | self.invokes.append({'invoke': event.invoke, 'ticker': identify_payload(event.payload)}) 185 | clear_queue(self.control_queue) 186 | self.connection.conn.force_close() 187 | self.control_queue.put(ReconnectEvent(None)) 188 | return 189 | else: 190 | if self._handle_subscribe_for_ticker(invoke, payload): 191 | return True 192 | elif invoke == BittrexMethods.GET_AUTH_CONTENT: 193 | # The reconnection procedures puts the key in a tuple and it fails, hence the little quick fix. 194 | key = identify_payload(payload) 195 | self.connection.corehub.server.invoke(invoke, key) 196 | self.invokes.append({'invoke': invoke, 'ticker': key}) 197 | logger.info('Retrieving authentication challenge.') 198 | elif invoke == BittrexMethods.AUTHENTICATE: 199 | key = payload[0] 200 | challenge = payload[1] 201 | if type(key) is not str: 202 | logger.error('API key is not transferred. Private authentication will fail.' 203 | '\nAPI key type is {}' 204 | '\nReport to https://github.com/slazarov/python-bittrex-websocket.'.format(type(key))) 205 | if type(challenge) is not str: 206 | logger.error('Challenge is not transferred. Private authentication will fail.' 207 | '\nChallenge type is {}' 208 | '\nReport to https://github.com/slazarov/python-bittrex-websocket.'.format( 209 | type(challenge))) 210 | self.connection.corehub.server.invoke(invoke, key, challenge) 211 | logger.info('Challenge retrieved. Sending authentication. Awaiting messages...') 212 | # No need to append invoke list, because AUTHENTICATE is called from successful GET_AUTH_CONTENT. 213 | else: 214 | self.invokes.append({'invoke': invoke, 'ticker': None}) 215 | self.connection.corehub.server.invoke(invoke) 216 | logger.info('Successfully invoked [{}].'.format(invoke)) 217 | 218 | def _handle_subscribe_for_ticker(self, invoke, payload): 219 | if invoke in [BittrexMethods.SUBSCRIBE_TO_EXCHANGE_DELTAS, BittrexMethods.QUERY_EXCHANGE_STATE]: 220 | for ticker in payload[0]: 221 | self.invokes.append({'invoke': invoke, 'ticker': ticker}) 222 | self.connection.corehub.server.invoke(invoke, ticker) 223 | logger.info('Successfully subscribed to [{}] for [{}].'.format(invoke, ticker)) 224 | return True 225 | return False 226 | 227 | # ============== 228 | # Public Methods 229 | # ============== 230 | 231 | def subscribe_to_exchange_deltas(self, tickers): 232 | if type(tickers) is list: 233 | invoke = BittrexMethods.SUBSCRIBE_TO_EXCHANGE_DELTAS 234 | event = SubscribeEvent(invoke, tickers) 235 | self.control_queue.put(event) 236 | else: 237 | raise TypeError(ErrorMessages.INVALID_TICKER_INPUT) 238 | 239 | def subscribe_to_summary_deltas(self): 240 | invoke = BittrexMethods.SUBSCRIBE_TO_SUMMARY_DELTAS 241 | event = SubscribeEvent(invoke, None) 242 | self.control_queue.put(event) 243 | 244 | def subscribe_to_summary_lite_deltas(self): 245 | invoke = BittrexMethods.SUBSCRIBE_TO_SUMMARY_LITE_DELTAS 246 | event = SubscribeEvent(invoke, None) 247 | self.control_queue.put(event) 248 | 249 | def query_summary_state(self): 250 | invoke = BittrexMethods.QUERY_SUMMARY_STATE 251 | event = SubscribeEvent(invoke, None) 252 | self.control_queue.put(event) 253 | 254 | def query_exchange_state(self, tickers): 255 | if type(tickers) is list: 256 | invoke = BittrexMethods.QUERY_EXCHANGE_STATE 257 | event = SubscribeEvent(invoke, tickers) 258 | self.control_queue.put(event) 259 | else: 260 | raise TypeError(ErrorMessages.INVALID_TICKER_INPUT) 261 | 262 | def authenticate(self, api_key, api_secret): 263 | self.credentials = {'api_key': api_key, 'api_secret': api_secret} 264 | event = SubscribeEvent(BittrexMethods.GET_AUTH_CONTENT, api_key) 265 | self.control_queue.put(event) 266 | 267 | def disconnect(self): 268 | self.control_queue.put(CloseEvent()) 269 | try: 270 | [thread.join() for thread in reversed(self.threads)] 271 | except RuntimeError: 272 | # If disconnect is called within, the ControlQueueThread will try to join itself 273 | # RuntimeError: cannot join current thread 274 | pass 275 | 276 | # ======================= 277 | # Private Channel Methods 278 | # ======================= 279 | 280 | def _on_public(self, args): 281 | msg = process_message(args) 282 | if 'D' in msg: 283 | if len(msg['D'][0]) > 3: 284 | msg['invoke_type'] = BittrexMethods.SUBSCRIBE_TO_SUMMARY_DELTAS 285 | else: 286 | msg['invoke_type'] = BittrexMethods.SUBSCRIBE_TO_SUMMARY_LITE_DELTAS 287 | else: 288 | msg['invoke_type'] = BittrexMethods.SUBSCRIBE_TO_EXCHANGE_DELTAS 289 | self._on_public_callback.on_change(msg) 290 | 291 | def _on_private(self, args): 292 | self._on_private_callback.on_change(process_message(args)) 293 | 294 | def _on_debug(self, **kwargs): 295 | if self.connection.conn.started is False: 296 | self.connection.conn.started = True 297 | # `QueryExchangeState`, `QuerySummaryState` and `GetAuthContext` are received in the debug channel. 298 | self._is_query_invoke(kwargs) 299 | 300 | def _is_query_invoke(self, kwargs): 301 | if 'R' in kwargs and type(kwargs['R']) is not bool: 302 | invoke = self.invokes[int(kwargs['I'])]['invoke'] 303 | if invoke == BittrexMethods.GET_AUTH_CONTENT: 304 | signature = create_signature(self.credentials['api_secret'], kwargs['R']) 305 | event = SubscribeEvent(BittrexMethods.AUTHENTICATE, self.credentials['api_key'], signature) 306 | self.control_queue.put(event) 307 | else: 308 | msg = process_message(kwargs['R']) 309 | if msg is not None: 310 | msg['invoke_type'] = invoke 311 | # Assign ticker name before Bittrex fixes that payload 312 | msg['ticker'] = self.invokes[int(kwargs['I'])]['ticker'] 313 | self._on_public_callback.on_change(msg) 314 | 315 | # ====================== 316 | # Public Channel Methods 317 | # ====================== 318 | 319 | def on_public(self, msg): 320 | pass 321 | 322 | def on_private(self, msg): 323 | pass 324 | 325 | def on_error(self, args): 326 | logger.error(args) 327 | 328 | # ============= 329 | # Other Methods 330 | # ============= 331 | 332 | @staticmethod 333 | def enable_log(file_name=None): 334 | """ 335 | Enables logging. 336 | :param file_name: The name of the log file, located in the same directory as the executing script. 337 | :type file_name: str 338 | """ 339 | add_stream_logger(file_name=file_name) 340 | 341 | @staticmethod 342 | def disable_log(): 343 | """ 344 | Disables logging. 345 | """ 346 | remove_stream_logger() 347 | -------------------------------------------------------------------------------- /docker/.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | LICENSE 4 | VERSION 5 | README.md 6 | Changelog.md 7 | Makefile 8 | docker-compose.yml 9 | docs 10 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as a parent image 2 | FROM python:3.6.3-alpine 3 | 4 | RUN apk update && apk add --no-cache --virtual .build-deps \ 5 | g++ make libffi-dev openssl-dev git && \ 6 | apk --update add nodejs && \ 7 | pip install 'cython>=0.25' && \ 8 | pip install git+https://github.com/slazarov/python-bittrex-websocket.git@ && \ 9 | apk del .build-deps && \ 10 | rm -rf /var/cache/apk/* 11 | 12 | # Set the working directory to /app 13 | WORKDIR /app 14 | 15 | # Copy the current directory contents into the container at /app 16 | ADD . /app 17 | 18 | # Run app.py when the container launches 19 | CMD ["python", "example.py"] 20 | -------------------------------------------------------------------------------- /docker/example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # bittrex_websocket/examples/order_book.py 5 | # Stanislav Lazarov 6 | 7 | # Sample script to show how the following methods work: 8 | # • subscribe_to_orderbook() 9 | # • get_order_book_sync_state() 10 | # • get_order_book() 11 | # Overview: 12 | # Subscribes to the order book for N tickers. 13 | # When all the order books are synced, prints the Bid quotes for each ticker at depth level 0 and disconnects 14 | 15 | from __future__ import print_function 16 | 17 | from time import sleep 18 | 19 | from bittrex_websocket.websocket_client import BittrexSocket 20 | 21 | 22 | def main(): 23 | class MySocket(BittrexSocket): 24 | def on_orderbook(self, msg): 25 | print('[OrderBook]: {}'.format(msg['MarketName'])) 26 | 27 | # Create the socket instance 28 | ws = MySocket() 29 | # Enable logging 30 | ws.enable_log('docker_log') 31 | # Define tickers 32 | tickers = ['BTC-ETH', 'BTC-NEO', 'BTC-ZEC', 'ETH-NEO', 'ETH-ZEC'] 33 | # Subscribe to live order book 34 | ws.subscribe_to_orderbook(tickers) 35 | 36 | while True: 37 | i = 0 38 | sync_states = ws.get_order_book_sync_state() 39 | for state in sync_states.values(): 40 | if state == 3: 41 | i += 1 42 | if i == len(tickers): 43 | print('We are fully synced. Hooray!') 44 | for ticker in tickers: 45 | ob = ws.get_order_book(ticker) 46 | name = ob['MarketName'] 47 | quantity = str(ob['Buys'][0]['Quantity']) 48 | price = str(ob['Buys'][0]['Rate']) 49 | print('Ticker: ' + name + ', Bids depth 0: ' + quantity + '@' + price) 50 | ws.disconnect() 51 | break 52 | else: 53 | sleep(1) 54 | 55 | 56 | if __name__ == "__main__": 57 | main() 58 | -------------------------------------------------------------------------------- /examples/order_book.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # /examples/order_book.py 5 | # Stanislav Lazarov 6 | 7 | 8 | from __future__ import print_function 9 | from time import sleep 10 | from bittrex_websocket import OrderBook 11 | 12 | 13 | def main(): 14 | class MySocket(OrderBook): 15 | def on_ping(self, msg): 16 | print('Received order book update for {}'.format(msg)) 17 | 18 | # Create the socket instance 19 | ws = MySocket() 20 | # Enable logging 21 | ws.enable_log() 22 | # Define tickers 23 | tickers = ['BTC-ETH'] 24 | # Subscribe to order book updates 25 | ws.subscribe_to_orderbook(tickers) 26 | 27 | while True: 28 | sleep(10) 29 | book = ws.get_order_book('BTC-ETH') 30 | print(book[u'S'][0]) 31 | else: 32 | pass 33 | 34 | 35 | if __name__ == "__main__": 36 | main() 37 | -------------------------------------------------------------------------------- /examples/record_trades.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # /examples/record_trades.py 5 | # Stanislav Lazarov 6 | 7 | # Sample script showing how subscribe_to_exchange_deltas() works. 8 | 9 | # Overview: 10 | # --------- 11 | # 1) Creates custom trade_history dict. 12 | # 2) When an order is executed, the fill is recorded in trade_history. 13 | # 3) When each ticker has received an order, the script disconnects. 14 | 15 | from __future__ import print_function 16 | from time import sleep 17 | from bittrex_websocket import BittrexSocket, BittrexMethods 18 | 19 | 20 | def main(): 21 | class MySocket(BittrexSocket): 22 | 23 | def on_public(self, msg): 24 | # Create entry for the ticker in the trade_history dict 25 | if msg['invoke_type'] == BittrexMethods.SUBSCRIBE_TO_EXCHANGE_DELTAS: 26 | if msg['M'] not in trade_history: 27 | trade_history[msg['M']] = [] 28 | # Add history nounce 29 | trade_history[msg['M']].append(msg) 30 | # Ping 31 | print('[Trades]: {}'.format(msg['M'])) 32 | 33 | # Create container 34 | trade_history = {} 35 | # Create the socket instance 36 | ws = MySocket() 37 | # Enable logging 38 | ws.enable_log() 39 | # Define tickers 40 | tickers = ['BTC-ETH', 'BTC-NEO', 'BTC-ZEC', 'ETH-NEO', 'ETH-ZEC'] 41 | # Subscribe to trade fills 42 | ws.subscribe_to_exchange_deltas(tickers) 43 | 44 | while len(set(tickers) - set(trade_history)) > 0: 45 | sleep(1) 46 | else: 47 | for ticker in trade_history.keys(): 48 | print('Printing {} trade history.'.format(ticker)) 49 | for trade in trade_history[ticker]: 50 | print(trade) 51 | ws.disconnect() 52 | # sleep(10000) 53 | 54 | 55 | if __name__ == "__main__": 56 | main() 57 | -------------------------------------------------------------------------------- /examples/ticker_updates.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # /examples/ticker_updates.py 5 | # Stanislav Lazarov 6 | 7 | # Sample script showing how subscribe_to_exchange_deltas() works. 8 | 9 | # Overview: 10 | # --------- 11 | # 1) Creates a custom ticker_updates_container dict. 12 | # 2) Subscribes to N tickers and starts receiving market data. 13 | # 3) When information is received, checks if the ticker is 14 | # in ticker_updates_container and adds it if not. 15 | # 4) Disconnects when it has data information for each ticker. 16 | 17 | from bittrex_websocket.websocket_client import BittrexSocket 18 | from time import sleep 19 | 20 | 21 | def main(): 22 | class MySocket(BittrexSocket): 23 | 24 | def on_public(self, msg): 25 | name = msg['M'] 26 | if name not in ticker_updates_container: 27 | ticker_updates_container[name] = msg 28 | print('Just received market update for {}.'.format(name)) 29 | 30 | # Create container 31 | ticker_updates_container = {} 32 | # Create the socket instance 33 | ws = MySocket() 34 | # Enable logging 35 | ws.enable_log() 36 | # Define tickers 37 | tickers = ['BTC-ETH', 'BTC-NEO', 'BTC-ZEC', 'ETH-NEO', 'ETH-ZEC'] 38 | # Subscribe to ticker information 39 | for ticker in tickers: 40 | sleep(0.01) 41 | ws.subscribe_to_exchange_deltas([ticker]) 42 | 43 | # Users can also subscribe without introducing delays during invoking but 44 | # it is the recommended way when you are subscribing to a large list of tickers. 45 | # ws.subscribe_to_exchange_deltas(tickers) 46 | 47 | while len(ticker_updates_container) < len(tickers): 48 | sleep(1) 49 | else: 50 | print('We have received updates for all tickers. Closing...') 51 | ws.disconnect() 52 | sleep(10) 53 | 54 | 55 | if __name__ == "__main__": 56 | main() 57 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests[security]==2.20.0 2 | Events==0.3 3 | websocket-client>=0.53.0 4 | signalr-client-threads -------------------------------------------------------------------------------- /resources/README_OLD.md: -------------------------------------------------------------------------------- 1 | # bittrex-websocket 2 | **Python** websocket client ([SignalR](https://pypi.python.org/pypi/signalr-client/0.0.7)) for getting live streaming data from [Bittrex Exchange](http://bittrex.com). 3 | 4 | The library is mainly written in Python3 but should support Python2 with the same functionality, please report any issues. 5 | 6 | If you prefer asyncio (Python>=3.5), try my other library https://github.com/slazarov/python-bittrex-websocket-aio. 7 | 8 | # My plans for the websocket client 9 | 10 | Bittrex released their [official beta websocket documentation](https://github.com/Bittrex/beta) on 27-March-2018. 11 | The major changes were the removal of the need to bypass Cloudflare and the introduction of new public (`Lite Summary Delta`) and private (`Balance Delta` & `Order Delta`) channels. 12 | 13 | Following that, I decided to repurpose the client as a higher level Bittrex API which users can use to build on. The major changes are: 14 | 15 | * ~~Existing methods will be restructured in order to mimic the official ones, i.e `subscribe_to_orderbook_update` will become `subscribe_to_exchange_deltas`. This would make referencing the official documentation more clear and will reduce confusion.~~ 16 | * ~~`QueryExchangeState` will become a public method so that users can invoke it freely.~~ 17 | * The method `subscribe_to_orderbook` will be removed and instead placed as a separate module. Before the latter happens, users can use the legacy library. 18 | * ~~Private, account specific methods will be implemented, i.e `Balance Delta` & `Order Delta`~~ 19 | * ~~Replacement of the legacy `on_channels` with only two channels for the public and private streams.~~ 20 | 21 | My goal is to maintain the interface of the two socket versions as similar as possible and leave the choice between async/gevent and Python2/3 to the users. 22 | Saying that, I still prefer the async version. 23 | 24 | ### Disclaimer 25 | 26 | *I am not associated with Bittrex. Use the library at your own risk, I don't bear any responsibility if you end up losing your money.* 27 | 28 | *The code is licensed under the MIT license. Please consider the following message:* 29 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 31 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 32 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 33 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 34 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE 35 | 36 | # Table of contents 37 | * [bittrex\-websocket](#bittrex-websocket) 38 | * [Table of contents](#table-of-contents) 39 | * [What can I use it for?](#what-can-i-use-it-for) 40 | * [Notices](#notices) 41 | * [Road map](#road-map) 42 | * [Dependencies](#dependencies) 43 | * [Installation](#installation) 44 | * [Methods](#methods) 45 | * [Subscribe Methods](#subscribe-methods) 46 | * [Unsubscribe Methods](#unsubscribe-methods) 47 | * [Other Methods](#other-methods) 48 | * [Message channels](#message-channels) 49 | * [Sample usage](#sample-usage) 50 | * [Change log](#change-log) 51 | * [Other libraries](#other-libraries) 52 | * [Support](#support) 53 | * [Motivation](#motivation) 54 | 55 | 56 | # What can I use it for? 57 | You can use it for various purposes, some examples include: 58 | * maintaining live order book 59 | * recording trade history 60 | * analysing order flow 61 | 62 | Use your imagination. 63 | 64 | # Notices 65 | **14/04/2018** 66 | 67 | The async version of the socket has been released: [python-bittrex-websocket-aio](https://github.com/slazarov/python-bittrex-websocket-aio). 68 | 69 | **27/03/2018** 70 | 71 | Bittrex has published officicial beta [documentation](https://github.com/Bittrex/beta). 72 | 73 | # Road map 74 | 75 | 76 | # Dependencies 77 | To successfully install the package the following dependencies must be met: 78 | 79 | * [requests[security]](https://github.com/requests/requests) 80 | * g++, make, libffi-dev, openssl-dev 81 | 82 | I have added a Dockerfile for а quick setup. Please check the docker folder. The example.py is not always up to date. 83 | 84 | I am only adding this as a precaution, in most case you will not have to do anything at all as these are prepackaged with your python installation. 85 | 86 | # Installation 87 | 88 | The library can be installed through Github and PyPi. For the latest updates, use Github. 89 | 90 | ```python 91 | pip install git+https://github.com/slazarov/python-bittrex-websocket.git 92 | pip install git+https://github.com/slazarov/python-bittrex-websocket.git@next-version-number 93 | pip install bittrex-websocket 94 | ``` 95 | # Methods 96 | #### Custom URL 97 | Custom URLs can be passed to the client upon instantiating. 98 | ```python 99 | # 'https://socket.bittrex.com/signalr' is currently Cloudflare protected 100 | # 'https://beta.bittrex.com/signalr' (DEFAULT) is not 101 | 102 | # Create the socket instance 103 | ws = MySocket(url=None) 104 | # rest of your code 105 | ``` 106 | 107 | #### Subscribe Methods 108 | ```python 109 | def subscribe_to_exchange_deltas(self, tickers): 110 | """ 111 | Allows the caller to receive real-time updates to the state of a SINGLE market. 112 | Upon subscribing, the callback will be invoked with market deltas as they occur. 113 | 114 | This feed only contains updates to exchange state. To form a complete picture of 115 | exchange state, users must first call QueryExchangeState and merge deltas into 116 | the data structure returned in that call. 117 | 118 | :param tickers: A list of tickers you are interested in. 119 | :type tickers: [] 120 | """ 121 | 122 | 123 | def subscribe_to_summary_deltas(self): 124 | """ 125 | Allows the caller to receive real-time updates of the state of ALL markets. 126 | Upon subscribing, the callback will be invoked with market deltas as they occur. 127 | 128 | Summary delta callbacks are verbose. A subset of the same data limited to the 129 | market name, the last price, and the base currency volume can be obtained via 130 | `subscribe_to_summary_lite_deltas`. 131 | 132 | https://github.com/Bittrex/beta#subscribetosummarydeltas 133 | """ 134 | 135 | def subscribe_to_summary_lite_deltas(self): 136 | """ 137 | Similar to `subscribe_to_summary_deltas`. 138 | Shows only market name, last price and base currency volume. 139 | 140 | """ 141 | 142 | def query_summary_state(self): 143 | """ 144 | Allows the caller to retrieve the full state for all markets. 145 | """ 146 | 147 | def query_exchange_state(self, tickers): 148 | """ 149 | Allows the caller to retrieve the full order book for a specific market. 150 | 151 | :param tickers: A list of tickers you are interested in. 152 | :type tickers: [] 153 | """ 154 | 155 | def authenticate(self, api_key, api_secret): 156 | """ 157 | Verifies a user’s identity to the server and begins receiving account-level notifications 158 | 159 | :param api_key: Your api_key with the relevant permissions. 160 | :type api_key: str 161 | :param api_secret: Your api_secret with the relevant permissions. 162 | :type api_secret: str 163 | """ 164 | ``` 165 | 166 | #### Other Methods 167 | 168 | ```python 169 | def disconnect(self): 170 | """ 171 | Disconnects the socket. 172 | """ 173 | 174 | def enable_log(file_name=None): 175 | """ 176 | Enables logging. 177 | 178 | :param file_name: The name of the log file, located in the same directory as the executing script. 179 | :type file_name: str 180 | """ 181 | 182 | def disable_log(): 183 | """ 184 | Disables logging. 185 | """ 186 | ``` 187 | 188 | # Message channels 189 | ```python 190 | def on_public(self, msg): 191 | # The main channel for all public methods. 192 | 193 | def on_private(self, msg): 194 | # The main channel for all private methods. 195 | 196 | def on_error(self, error): 197 | # Receive error message from the SignalR connection. 198 | 199 | ``` 200 | 201 | # Sample usage 202 | Check the examples folder. 203 | 204 | # Change log 205 | 1.0.1.0 - 22/04/2018 206 | * Custom urls can now be passed to the client 207 | * If `cfscrape` is installed, the client will automatically use it 208 | 209 | 1.0.0.0 - 15/04/2018 210 | * As per the [road map](#my-plans-for-the-websocket-client) 211 | 212 | 0.0.7.3 - 06/04/2018 213 | * Set cfscrape >=1.9.5 214 | 215 | 0.0.7.2 - 31/03/2018 216 | * Added third connection URL. 217 | 218 | 0.0.7.1 - 31/03/2018 219 | * Removed wsaccel: no particular socket benefits 220 | * Fixed RecursionError as per [Issue #52](https://github.com/slazarov/python-bittrex-websocket/issues/52) 221 | 222 | 0.0.7.0 - 25/02/2018 223 | * New reconnection methods implemented. Problem was within `gevent`, because connection failures within it are not raised in the main script. 224 | * Added wsaccel for better socket performance. 225 | * Set websocket-client minimum version to 0.47.0 226 | 227 | 0.0.6.4 - 24/02/2018 228 | * Fixed order book syncing bug when more than 1 connection is online due to wrong connection/thread name. 229 | 230 | 0.0.6.3 - 18/02/2018 231 | * Major changes to how the code handles order book syncing. Syncing is done significantly faster than previous versions, i.e full sync of all Bittrex tickers takes ca. 4 minutes. 232 | * Fixed `on_open` bug as per [Issue #21](https://github.com/slazarov/python-bittrex-websocket/issues/21) 233 | 234 | 0.0.6.2.2 235 | * Update cfscrape>=1.9.2 and gevent>=1.3a1 236 | * Reorder imports in websocket_client to safeguard against SSL recursion errors. 237 | 238 | 0.0.6.2 239 | * Every 5400s (1hr30) the script will force reconnection. 240 | * Every reconnection (including the above) will be done with a fresh cookie 241 | * Upon reconnection the script will check if the connection has been running for more than 600s (10mins). If it has been running for less it will use the backup url. 242 | 243 | 0.0.6.1 244 | * Set websocket-client==0.46.0 245 | 246 | 0.0.6 247 | * Reconnection - Experimental 248 | * Fixed a bug when subscribing to multiple subscription types at once resulted in opening unnecessary connections even though there is sufficient capacity in the existing [Commit 7fd21c](https://github.com/slazarov/python-bittrex-websocket/commit/7fd21cad87a8bd7c88070bab0fd5774b0324332e) 249 | * Numerous code optimizations 250 | 251 | 0.0.5.1 252 | * Updated cfscrape minimum version requirement ([Issue #12](https://github.com/slazarov/python-bittrex-websocket/issues/12)). 253 | 254 | 0.0.5 255 | * Fixed [Issue #9](https://github.com/slazarov/python-bittrex-websocket/issues/9) relating to `subscribe_to_orderbook_update` handling in internal method `_on_tick_update` 256 | * Added custom logger as per [PR #10](https://github.com/slazarov/python-bittrex-websocket/issues/10) and [Issue #8](https://github.com/slazarov/python-bittrex-websocket/issues/8) in order to avoid conflicts with other `basicConfig` setups 257 | * Added two new methods `enable_log` and `disable_log`. Check [Other Methods](#other-methods). 258 | * Logging is now disabled on startup. You have to enable it. 259 | * **Experimental**: Calling `subscribe_to_ticker_update` without a specified ticker subscribes to all tickers in the message stream ([Issue #4](https://github.com/slazarov/python-bittrex-websocket/issues/4)). 260 | * Minor code optimizations (removed unnecessary class Common) 261 | 262 | 0.0.4 - Changed the behaviour of how on_ticker_update channel works: 263 | The message now contains a single ticker instead of a dictionary of all subscribed tickers. 264 | 265 | 0.0.3 - Removed left over code from initial release version that was throwing errors (had no effect on performance). 266 | 267 | 0.0.2 - Major improvements: 268 | 269 | * Additional un/subscribe and order book sync state querying methods added. 270 | * Better connection and thread management. 271 | * Code optimisations 272 | * Better code documentation 273 | * Added additional connection URLs 274 | 275 | 0.0.1 - Initial release on github. 276 | 277 | # Other libraries 278 | **[Python Bittrex Autosell](https://github.com/slazarov/python-bittrex-autosell)** 279 | 280 | Python CLI tool to auto sell coins on Bittrex. 281 | 282 | It is used in the cases when you want to auto sell a specific coin for another, but there is no direct market, so you have to use an intermediate market. 283 | 284 | # Motivation 285 | I am fairly new to Python and in my experience the best way to learn something is through actual practice. At the same time I am currently actively trading on Bittrex, hence the reason why I decided to build the Bittrex websocket client. I am publishing my code for a few reasons. Firstly, I want to make a contribution to the community, secondly the code needs lots of improvements and it would be great to work on it as a team, thirdly I haven't seen any other Python Bittrex websocket clients so far. 286 | 287 | I have been largely motivated by the following projects and people: 288 | 289 | * Daniel Paquin: [gdax-python](https://github.com/danpaquin/gdax-python) - a websocket client for GDAX. The project really helped me around using threads and structuring the code. 290 | 291 | * [David Parlevliet](https://github.com/dparlevliet) - saw his SignalR code initially which included Bittrex specific commands. Saved me a lot of time in researching. 292 | 293 | * Eric Somdahl: [python-bittrex](https://github.com/ericsomdahl/python-bittrex) - great python bindings for Bittrex. Highly recommend it, I use it in conjuction with the websocket client. -------------------------------------------------------------------------------- /resources/archieved_changelog.txt: -------------------------------------------------------------------------------- 1 | 0.0.7.3 - 06/04/2018 2 | - Set cfscrape >=1.9.5 3 | 4 | 0.0.7.2 - 31/03/2018 5 | - Added third connection URL. 6 | 7 | 0.0.7.1 - 31/03/2018 8 | -Removed wsaccel: no particular socket benefits 9 | - Fixed RecursionError as per Issue #52 10 | 11 | 0.0.7.0 - 25/02/2018 12 | - New reconnection methods implemented. Problem was within gevent, because connection failures within it are not raised in the main script. 13 | - Added wsaccel for better socket performance. 14 | - Set websocket-client minimum version to 0.47.0 15 | 16 | 0.0.6.4 - 24/02/2018 17 | - Fixed order book syncing bug when more than 1 connection is online due to wrong connection/thread name. 18 | 19 | 0.0.6.3 - 18/02/2018 20 | - Major changes to how the code handles order book syncing. Syncing is done significantly faster than previous versions, i.e full sync of all Bittrex tickers takes ca. 4 minutes. 21 | - Fixed on_open bug as per Issue #21 22 | 23 | 0.0.6.2.2 24 | - Update cfscrape>=1.9.2 and gevent>=1.3a1 25 | - Reorder imports in websocket_client to safeguard against SSL recursion errors. 26 | 27 | 0.0.6.2 28 | - Every 5400s (1hr30) the script will force reconnection. 29 | - Every reconnection (including the above) will be done with a fresh cookie 30 | - Upon reconnection the script will check if the connection has been running for more than 600s (10mins). If it has been running for less it will use the backup url. 31 | 32 | 0.0.6.1 33 | - Set websocket-client==0.46.0 34 | 35 | 0.0.6 36 | - Reconnection - Experimental 37 | - Fixed a bug when subscribing to multiple subscription types at once resulted in opening unnecessary connections even though there is sufficient capacity in the existing Commit 7fd21c 38 | - Numerous code optimizations 39 | 40 | 0.0.5.1 41 | - Updated cfscrape minimum version requirement (Issue #12). 42 | 43 | 0.0.5 44 | - Fixed Issue #9 relating to subscribe_to_orderbook_update handling in internal method _on_tick_update 45 | - Added custom logger as per PR #10 and Issue #8 in order to avoid conflicts with other basicConfig setups 46 | - Added two new methods enable_log and disable_log. Check Other Methods. 47 | - Logging is now disabled on startup. You have to enable it. 48 | - Experimental: Calling subscribe_to_ticker_update without a specified ticker subscribes to all tickers in the message stream (Issue #4). 49 | - Minor code optimizations (removed unnecessary class Common) 50 | 51 | 0.0.4 52 | - Changed the behaviour of how on_ticker_update channel works: The message now contains a single ticker instead of a dictionary of all subscribed tickers. 53 | 54 | 0.0.3 55 | - Removed left over code from initial release version that was throwing errors (had no effect on performance). 56 | 57 | 0.0.2 58 | - Additional un/subscribe and order book sync state querying methods added. 59 | - Better connection and thread management. 60 | - Code optimisations 61 | - Better code documentation 62 | - Added additional connection URLs 63 | 64 | 0.0.1 - Initial release on github. -------------------------------------------------------------------------------- /resources/py_btrx.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | 4 | [metadata] 5 | description-file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from setuptools import setup, find_packages 4 | 5 | install_requires = \ 6 | [ 7 | 'requests[security]==2.20.0', 8 | 'Events==0.3', 9 | 'websocket-client>=0.53.0', 10 | 'signalr-client-threads' 11 | ] 12 | 13 | setup( 14 | name='bittrex_websocket', 15 | version='1.0.6.3', 16 | author='Stanislav Lazarov', 17 | author_email='s.a.lazarov@gmail.com', 18 | license='MIT', 19 | url='https://github.com/slazarov/python-bittrex-websocket', 20 | packages=find_packages(exclude=['tests*']), 21 | install_requires=install_requires, 22 | description='The unofficial Python websocket client for the Bittrex Cryptocurrency Exchange', 23 | download_url='https://github.com/slazarov/python-bittrex-websocket.git', 24 | keywords=['bittrex', 'bittrex-websocket', 'orderbook', 'trade', 'bitcoin', 'ethereum', 'BTC', 'ETH', 'client', 25 | 'websocket', 'exchange', 'crypto', 'currency', 'trading'], 26 | classifiers=[ 27 | 'Development Status :: 3 - Alpha', 28 | 'Intended Audience :: Developers', 29 | 'Intended Audience :: Financial and Insurance Industry', 30 | 'Intended Audience :: Information Technology', 31 | 'Topic :: Software Development :: Libraries :: Python Modules', 32 | 'License :: OSI Approved :: MIT License', 33 | 'Programming Language :: Python', 34 | 'Programming Language :: Python :: 2', 35 | 'Programming Language :: Python :: 2.7', 36 | 'Programming Language :: Python :: 3', 37 | 'Programming Language :: Python :: 3.3', 38 | 'Programming Language :: Python :: 3.4', 39 | 'Programming Language :: Python :: 3.5', 40 | 'Programming Language :: Python :: 3.6' 41 | ] 42 | ) 43 | --------------------------------------------------------------------------------