From c79163ea2eef74f94c700cecbd0ff3105d40f536 Mon Sep 17 00:00:00 2001 From: Oleg Nedbaylo Date: Sun, 28 Apr 2019 08:50:11 +0200 Subject: [PATCH 01/10] ons-deribit renamed to ssc2ce --- README.md | 23 +++++++++++++---------- examples/basic_example.py | 2 +- examples/private.py | 2 +- setup.py | 8 ++++---- {deribit => ssc2ce}/VERSION.py | 0 {deribit => ssc2ce}/__init__.py | 0 ssc2ce/common.py | 8 ++++++++ {deribit => ssc2ce}/deribit.py | 27 ++++++--------------------- {deribit => ssc2ce}/session.py | 0 {deribit => ssc2ce}/utils.py | 8 ++++++++ 10 files changed, 41 insertions(+), 37 deletions(-) rename {deribit => ssc2ce}/VERSION.py (100%) rename {deribit => ssc2ce}/__init__.py (100%) create mode 100644 ssc2ce/common.py rename {deribit => ssc2ce}/deribit.py (96%) rename {deribit => ssc2ce}/session.py (100%) rename {deribit => ssc2ce}/utils.py (89%) diff --git a/README.md b/README.md index 4677c74..76e4a91 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,23 @@ -# Simple package for Deribit API v2 websocket +# ssc2ce +A Set of Simple Connectors for access To Cryptocurrency Exchanges via websocket based on + [aiohttp](https://aiohttp.readthedocs.io) . + +## Deribit +### Description -## Description -The package use [aiohttp](https://aiohttp.readthedocs.io) API description look at [Deribit API v2 websocket](https://docs.deribit.com/v2/?python#json-rpc) -## Installation +### Installation Install ons-deribit with: ```bash $ pip install ons-deribit ``` -## Basic example +### Basic example ```python #!/usr/bin/env python import asyncio -from deribit import Deribit +from ssc2ce import Deribit conn = Deribit() @@ -49,7 +52,7 @@ except KeyboardInterrupt: print("Application closed by KeyboardInterrupt.") ``` -## Run examples from a clone +### Run examples from a clone If you clone repository you can run examples from the root directory. ```bash @@ -62,9 +65,9 @@ $ pip install python-dotenv ``` or make the corresponding changes, removed followed code. ```python - from dotenv import load_dotenv - dotenv_path = os.path.join(os.path.dirname(__file__), '.env') - load_dotenv(dotenv_path) +from dotenv import load_dotenv +dotenv_path = os.path.join(os.path.dirname(__file__), '.env') +load_dotenv(dotenv_path) ``` To run the private.py example, you must either fill in the .env file or set the environment variables DERIBIT_CLIENT_ID and DERIBIT_CLIENT_SECRET. Look at .env_default. ```bash diff --git a/examples/basic_example.py b/examples/basic_example.py index 3caff51..3a6688f 100644 --- a/examples/basic_example.py +++ b/examples/basic_example.py @@ -1,6 +1,6 @@ #!/usr/bin/env python import asyncio -from deribit import Deribit +from ssc2ce import Deribit conn = Deribit() diff --git a/examples/private.py b/examples/private.py index 2aaa073..03936b8 100644 --- a/examples/private.py +++ b/examples/private.py @@ -6,7 +6,7 @@ from uuid import uuid4 from dotenv import load_dotenv -from deribit.deribit import Deribit, AuthType +from ssc2ce.deribit import Deribit, AuthType dotenv_path = os.path.join(os.path.dirname(__file__), '.env') load_dotenv(dotenv_path) diff --git a/setup.py b/setup.py index ac45211..694e273 100644 --- a/setup.py +++ b/setup.py @@ -2,20 +2,20 @@ import setuptools -from deribit.VERSION import __version__ +from ssc2ce.VERSION import __version__ with open("README.md", "r") as fh: long_description = fh.read() setuptools.setup( - name='ons-deribit', + name='ssc2ce', version=__version__, author='Oleg Nedbaylo', author_email='olned64@gmail.com', - description='Simple Deribit API v2 on Websocket', + description='A Set of Simple Connectors for access To Cryptocurrency Exchanges', long_description=long_description, long_description_content_type="text/markdown", - url='https://github.com/olned/ons-deribit-ws-python', + url='https://github.com/olned/ssc2ce-python', packages=setuptools.find_packages(), install_requires=['aiohttp'], classifiers=[ diff --git a/deribit/VERSION.py b/ssc2ce/VERSION.py similarity index 100% rename from deribit/VERSION.py rename to ssc2ce/VERSION.py diff --git a/deribit/__init__.py b/ssc2ce/__init__.py similarity index 100% rename from deribit/__init__.py rename to ssc2ce/__init__.py diff --git a/ssc2ce/common.py b/ssc2ce/common.py new file mode 100644 index 0000000..ffd8ecc --- /dev/null +++ b/ssc2ce/common.py @@ -0,0 +1,8 @@ +from enum import IntEnum + + +class AuthType(IntEnum): + NONE = 0 + PASSWORD = 1 + CREDENTIALS = 2 + SIGNATURE = 3 diff --git a/deribit/deribit.py b/ssc2ce/deribit.py similarity index 96% rename from deribit/deribit.py rename to ssc2ce/deribit.py index 88e8587..cb3fdb7 100644 --- a/deribit/deribit.py +++ b/ssc2ce/deribit.py @@ -1,25 +1,10 @@ import logging -from enum import IntEnum import aiohttp -from deribit.session import SessionWrapper -from deribit.utils import resolve_route, hide_secret - - -class AuthType(IntEnum): - NONE = 0 - PASSWORD = 1 - CREDENTIALS = 2 - SIGNATURE = 3 - - -class IntId: - id = 0 - - def get_id(self): - self.id += 1 - return self.id +from .common import AuthType +from .session import SessionWrapper +from .utils import resolve_route, hide_secret, IntId class Deribit(SessionWrapper): @@ -33,8 +18,6 @@ class Deribit(SessionWrapper): on_response_error = None on_handle_response = None - ws_api = 'wss://test.deribit.com/ws/api/v2/' - requests = {} auth_params: dict = None @@ -45,12 +28,14 @@ def __init__(self, client_id: str = None, client_secret: str = None, scope: str = "session", + testnet: bool = True, auth_type: AuthType = AuthType.NONE, get_id=IntId().get_id): super().__init__() + self.ws_api = f"wss://{'test' if testnet else 'www'}.deribit.com/ws/api/v2/" self.get_id = get_id - self.logger = logging.getLogger(__name__ + '.Deribit') + self.logger = logging.getLogger(__name__) if auth_type & (AuthType.CREDENTIALS | AuthType.SIGNATURE): if client_secret is None or client_id is None: diff --git a/deribit/session.py b/ssc2ce/session.py similarity index 100% rename from deribit/session.py rename to ssc2ce/session.py diff --git a/deribit/utils.py b/ssc2ce/utils.py similarity index 89% rename from deribit/utils.py rename to ssc2ce/utils.py index f06a3ce..ea6da4a 100644 --- a/deribit/utils.py +++ b/ssc2ce/utils.py @@ -27,3 +27,11 @@ def hide_secret(request): data[key] = "***" return data + + +class IntId: + id = 0 + + def get_id(self): + self.id += 1 + return self.id From cfa049a6f47c47f14e2a9a3121ad5843ac7bcda2 Mon Sep 17 00:00:00 2001 From: Oleg Nedbaylo Date: Sun, 28 Apr 2019 09:17:54 +0200 Subject: [PATCH 02/10] Deribit examples were renamed --- README.md | 2 +- examples/{basic_example.py => deribit_basic_example.py} | 0 examples/{private.py => deribit_private.py} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename examples/{basic_example.py => deribit_basic_example.py} (100%) rename examples/{private.py => deribit_private.py} (100%) diff --git a/README.md b/README.md index 76e4a91..82dc214 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ If you clone repository you can run examples from the root directory. $ PYTHONPATH=.:$PYTHONPATH python examples/basic_example.py ``` -The private.py example uses [python-dotenv](https://github.com/theskumar/python-dotenv), you must either install it if you want the example to work right out of the box, +The deribit_private.py example uses [python-dotenv](https://github.com/theskumar/python-dotenv), you must either install it if you want the example to work right out of the box, ```bash $ pip install python-dotenv ``` diff --git a/examples/basic_example.py b/examples/deribit_basic_example.py similarity index 100% rename from examples/basic_example.py rename to examples/deribit_basic_example.py diff --git a/examples/private.py b/examples/deribit_private.py similarity index 100% rename from examples/private.py rename to examples/deribit_private.py From 739bf6bbf1c030ee4c41a10c751d3bb379ade280 Mon Sep 17 00:00:00 2001 From: Oleg Nedbaylo Date: Sun, 28 Apr 2019 09:19:01 +0200 Subject: [PATCH 03/10] exceptions fixed --- ssc2ce/deribit.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/ssc2ce/deribit.py b/ssc2ce/deribit.py index cb3fdb7..610a9f1 100644 --- a/ssc2ce/deribit.py +++ b/ssc2ce/deribit.py @@ -2,6 +2,7 @@ import aiohttp +from .exceptions import Ssc2ceError from .common import AuthType from .session import SessionWrapper from .utils import resolve_route, hide_secret, IntId @@ -24,7 +25,6 @@ class Deribit(SessionWrapper): last_message = None def __init__(self, - ws_api: str = None, client_id: str = None, client_secret: str = None, scope: str = "session", @@ -39,14 +39,10 @@ def __init__(self, if auth_type & (AuthType.CREDENTIALS | AuthType.SIGNATURE): if client_secret is None or client_id is None: - raise Exception(f" Authentication {str(auth_type)} need client_id and client_secret") + raise Ssc2ceError(f" Authentication {str(auth_type)} need client_id and client_secret") - # Todo: implement client_signature if auth_type == AuthType.SIGNATURE: - raise Exception(f" Authentication {str(auth_type)} still does not support. It is in my todo list.") - - if ws_api: - self.ws_api = ws_api + raise NotImplemented(f"Authentication {str(auth_type)} for Deribit is not implemented.") self.auth_type = auth_type self.client_id = client_id From 3cd15cc5db08a0b9dc3d1d2be718548c9b941672 Mon Sep 17 00:00:00 2001 From: Oleg Nedbaylo Date: Sun, 28 Apr 2019 09:19:18 +0200 Subject: [PATCH 04/10] Added Ssc2ceError --- ssc2ce/exceptions.py | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 ssc2ce/exceptions.py diff --git a/ssc2ce/exceptions.py b/ssc2ce/exceptions.py new file mode 100644 index 0000000..d1d2c8d --- /dev/null +++ b/ssc2ce/exceptions.py @@ -0,0 +1,2 @@ +class Ssc2ceError(Exception): + """Base class for errors.""" From b4cc0194396f7b3404e5ef8336ca604ed6864a7a Mon Sep 17 00:00:00 2001 From: Oleg Nedbaylo Date: Sun, 28 Apr 2019 10:13:20 +0200 Subject: [PATCH 05/10] examples updated --- examples/deribit_private.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/deribit_private.py b/examples/deribit_private.py index 03936b8..f21c671 100644 --- a/examples/deribit_private.py +++ b/examples/deribit_private.py @@ -12,7 +12,7 @@ load_dotenv(dotenv_path) logging.basicConfig(format='%(asctime)s %(name)s %(funcName)s %(levelname)s %(message)s', level=logging.WARNING) -logger = logging.getLogger("ons-derobit-ws-python-sample") +logger = logging.getLogger("deribit-private") client_id = os.environ.get('DERIBIT_CLIENT_ID') client_secret = os.environ.get('DERIBIT_CLIENT_SECRET') From 79de7ed4f25fab36828d6b7c7dca649018c249a6 Mon Sep 17 00:00:00 2001 From: Oleg Nedbaylo Date: Sun, 28 Apr 2019 11:39:04 +0200 Subject: [PATCH 06/10] bitfinex added --- examples/bitfinex_basic_example.py | 38 +++++++ ssc2ce/__init__.py | 1 + ssc2ce/bitfinex.py | 173 +++++++++++++++++++++++++++++ 3 files changed, 212 insertions(+) create mode 100644 examples/bitfinex_basic_example.py create mode 100644 ssc2ce/bitfinex.py diff --git a/examples/bitfinex_basic_example.py b/examples/bitfinex_basic_example.py new file mode 100644 index 0000000..2a5833c --- /dev/null +++ b/examples/bitfinex_basic_example.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +import asyncio +import logging + +from ssc2ce import Bitfinex + +logging.basicConfig(format='%(asctime)s %(name)s %(funcName)s %(levelname)s %(message)s', level=logging.INFO) +logger = logging.getLogger("ons-derobit-ws-python-sample") + +conn = Bitfinex() + + +async def handle_subscription(data): + print(data) + # method = data.get("method") + # if method and method == "subscription": + # if data["params"]["channel"].startswith("deribit_price_index"): + # index_name = data["params"]["data"]["index_name"] + # price = data["params"]["data"]["price"] + # print(f" Deribit Price Index {index_name.upper()}: {price}") + + +async def subscribe(): + await conn.subscribe({ + "channel": "ticker", + "symbol": "tBTCUSD" + }, handler=handle_subscription) + + +conn.on_connect_ws = subscribe +# conn.routes.insert(0, ("ticker", handle_subscription)) + +loop = asyncio.get_event_loop() + +try: + loop.run_until_complete(conn.run_receiver()) +except KeyboardInterrupt: + print("Application closed by KeyboardInterrupt.") diff --git a/ssc2ce/__init__.py b/ssc2ce/__init__.py index 5922489..2fe2194 100644 --- a/ssc2ce/__init__.py +++ b/ssc2ce/__init__.py @@ -1 +1,2 @@ from .deribit import Deribit +from .bitfinex import Bitfinex diff --git a/ssc2ce/bitfinex.py b/ssc2ce/bitfinex.py new file mode 100644 index 0000000..041f331 --- /dev/null +++ b/ssc2ce/bitfinex.py @@ -0,0 +1,173 @@ +import logging + +import aiohttp + +from .session import SessionWrapper +from .utils import resolve_route + +from enum import IntEnum + + +class Bitfinex(SessionWrapper): + class ConfigFlag(IntEnum): + TIMESTAMP = 32768 + SEQ_ALL = 65536 + CHECKSUM = 131072 + + class StatusFlag(IntEnum): + MAINTENANCE = 0 + OPERATIVE = 1 + + ws: aiohttp.ClientWebSocketResponse = None + on_connect_ws = None + on_close_ws = None + on_maintenance = None + on_conf = None + + ws_api = 'wss://api-pub.bitfinex.com/ws/2' + + last_message = None + is_connected = False + subscriptions = [] + channel_handlers = {} + + def __init__(self, + flags: ConfigFlag = ConfigFlag.TIMESTAMP | ConfigFlag.SEQ_ALL): + super().__init__() + + self.flags = flags + self.logger = logging.getLogger(__name__) + + self._timeout: aiohttp.ClientTimeout = aiohttp.ClientTimeout(total=20) + + self.on_message = self.handle_message + # self.on_connect = self.configure + self.routes = [ + ("subscribed", self.handle_subscribed), + ("info", self.handle_info), + ("conf", self.handle_conf), + # ("", self.warn_handler), + ] + + # self.channel_route = [] + + async def configure(self, flags: ConfigFlag = None): + if flags is not None: + self.flags = flags + + if self.flags is not None: + request = dict(event="conf", flags=self.flags) + await self.ws.send_json(request) + + async def subscribe(self, request, handler): + self.subscriptions.append((request, handler)) + await self.ws.send_json({ + "event": "subscribe", + **request + }) + + async def run_receiver(self): + self.ws = await self._session.ws_connect(self.ws_api) + + while self.ws and not self.ws.closed: + message = await self.ws.receive() + self.last_message = message + + if message.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.ERROR): + self.logger.warning(f"Connection close {repr(message)}") + if self.on_close_ws: + await self.on_close_ws() + + continue + if message.type == aiohttp.WSMsgType.CLOSING: + self.logger.debug(f"Connection closing {repr(message)}") + continue + + if self.on_message: + await self.on_message(message) + + async def handle_message(self, message: aiohttp.WSMessage): + if message.type == aiohttp.WSMsgType.TEXT: + data = message.json() + + if isinstance(data, list): + channel_id = data[0] + handler = self.channel_handlers.get(channel_id) + if handler: + await handler(data) + else: + self.logger.warning(f"Can't find handler for channel_id{channel_id}, {data}") + elif isinstance(data, dict): + if "event" in data: + await self.handle_event(data) + else: + self.logger.warning(f"Unknown message {message.data}") + else: + self.logger.warning(f"Unknown message {message.data}") + else: + self.logger.warning(f"Unknown type of message {repr(message)}") + + async def empty_handler(self, data): + pass + + async def handle_subscribed(self, message): + self.logger.info(f"receive subscribed: {message}") + idx = None + for i, s in enumerate(self.subscriptions): + if s[0].items() <= message.items(): + idx = i + self.channel_handlers[message["chanId"]] = s[1] + + if idx: + del self.subscriptions[idx] + + async def handle_conf(self, message): + """{'event': 'conf', 'status': 'OK', 'flags': 98304}""" + self.logger.info(f"{message}") + if self.on_conf: + await self.on_conf() + + async def handle_info(self, message): + """ + Handle an info message that contains the actual version of the websocket stream, along with a platform status + flag (1 for operative, 0 for maintenance + :param message: Info messages {'event': 'info', 'version': 2, 'serverId': ..., 'platform': {'status': 1}} + version: the actual version of the websocket stream must be 2 + status: a platform status flag (1 for operative, 0 for maintenance). + :return: + """ + self.logger.info(f"{message}") + + if message["version"] != 2: + raise NotImplemented(f"Bitfinex connector support only version 2 but receive {message}") + + if message["platform"]["status"] == 1: + if not self.is_connected: + await self.configure() + if self.on_connect_ws: + await self.on_connect_ws() + else: + if self.on_maintenance: + await self.on_maintenance(message) + else: + if self.on_maintenance: + await self.on_maintenance(message) + + async def warn_handler(self, message): + self.logger.warning(f"Unsupported message {message}") + + async def handle_event(self, message): + event = message["event"] + handler = resolve_route(event, self.routes) + + if handler: + return await handler(message) + + self.logger.warning(f"Unhandled event:{event}, message:{repr(message)} .") + return + + def close(self): + super()._close() + + async def stop(self): + await self.ws.close() From c8cb6d247a6c2d66eea616b9d4c963e398e9baae Mon Sep 17 00:00:00 2001 From: Oleg Nedbaylo Date: Sun, 28 Apr 2019 11:40:27 +0200 Subject: [PATCH 07/10] __version__ = "1.0.0" --- ssc2ce/VERSION.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ssc2ce/VERSION.py b/ssc2ce/VERSION.py index df12433..5becc17 100644 --- a/ssc2ce/VERSION.py +++ b/ssc2ce/VERSION.py @@ -1 +1 @@ -__version__ = "0.4.2" +__version__ = "1.0.0" From f475717976c152e6fbd5944b50719d7141c76bfc Mon Sep 17 00:00:00 2001 From: Oleg Nedbaylo Date: Sun, 28 Apr 2019 11:51:09 +0200 Subject: [PATCH 08/10] __version__ = "0.5.0" --- ssc2ce/VERSION.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ssc2ce/VERSION.py b/ssc2ce/VERSION.py index 5becc17..3d18726 100644 --- a/ssc2ce/VERSION.py +++ b/ssc2ce/VERSION.py @@ -1 +1 @@ -__version__ = "1.0.0" +__version__ = "0.5.0" From 695c4e94290f5626f168ee4c485f67e9681d0ac6 Mon Sep 17 00:00:00 2001 From: Oleg Nedbaylo Date: Sun, 28 Apr 2019 11:51:33 +0200 Subject: [PATCH 09/10] Readme.md updated --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 82dc214..fb60ef6 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,52 @@ A Set of Simple Connectors for access To Cryptocurrency Exchanges via websocket based on [aiohttp](https://aiohttp.readthedocs.io) . -## Deribit +## Installation +Install ssc2ce with: +```bash +$ pip install ssc2ce +``` + +## Bitfinex ### Description +API description look at [Websocket API v2](https://docs.bitfinex.com/v2/docs/ws-general) +### Basic example +```python +import asyncio +from ssc2ce import Bitfinex -API description look at [Deribit API v2 websocket](https://docs.deribit.com/v2/?python#json-rpc) +conn = Bitfinex() + + +async def handle_subscription(data): + print(data) + +async def subscribe(): + await conn.subscribe({ + "channel": "ticker", + "symbol": "tBTCUSD" + }, handler=handle_subscription) + + +conn.on_connect_ws = subscribe + +loop = asyncio.get_event_loop() + +try: + loop.run_until_complete(conn.run_receiver()) +except KeyboardInterrupt: + print("Application closed by KeyboardInterrupt.") -### Installation -Install ons-deribit with: -```bash -$ pip install ons-deribit ``` + +## Deribit +### Description + +API description look at [Deribit API v2 websocket](https://docs.deribit.com/v2/?python#json-rpc) + ### Basic example ```python -#!/usr/bin/env python import asyncio from ssc2ce import Deribit @@ -52,7 +84,7 @@ except KeyboardInterrupt: print("Application closed by KeyboardInterrupt.") ``` -### Run examples from a clone +## Run examples from a clone If you clone repository you can run examples from the root directory. ```bash From 5824c0749451dfd6d29f0cce41775ae89eb6dbba Mon Sep 17 00:00:00 2001 From: Oleg Nedbaylo Date: Sun, 28 Apr 2019 11:52:34 +0200 Subject: [PATCH 10/10] the bitfinex example updated --- examples/bitfinex_basic_example.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/examples/bitfinex_basic_example.py b/examples/bitfinex_basic_example.py index 2a5833c..251ee7b 100644 --- a/examples/bitfinex_basic_example.py +++ b/examples/bitfinex_basic_example.py @@ -12,12 +12,6 @@ async def handle_subscription(data): print(data) - # method = data.get("method") - # if method and method == "subscription": - # if data["params"]["channel"].startswith("deribit_price_index"): - # index_name = data["params"]["data"]["index_name"] - # price = data["params"]["data"]["price"] - # print(f" Deribit Price Index {index_name.upper()}: {price}") async def subscribe(): @@ -28,7 +22,6 @@ async def subscribe(): conn.on_connect_ws = subscribe -# conn.routes.insert(0, ("ticker", handle_subscription)) loop = asyncio.get_event_loop()