Skip to content

Commit

Permalink
Merge pull request #14 from olned/develop
Browse files Browse the repository at this point in the history
Deribit order book
  • Loading branch information
olned committed Jun 5, 2020
2 parents a447bab + a9bb0c9 commit 6564aea
Show file tree
Hide file tree
Showing 18 changed files with 279 additions and 13 deletions.
80 changes: 80 additions & 0 deletions examples/deribit_book.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env python
import asyncio
import json
import logging
from ssc2ce import Deribit
from ssc2ce.deribit.l2_book import L2Book

conn = Deribit()

pending = {}
books = {}

logging.basicConfig(format='%(asctime)s %(name)s %(funcName)s %(levelname)s %(message)s', level=logging.INFO)
logger = logging.getLogger("deribit-book")


async def handle_instruments(data: dict):
request_id = data["id"]
del pending[request_id]
print(json.dumps(data))
if not pending:
await subscribe_books(list(books.keys()))


async def handle_currencies(data: dict):
for currency in data["result"]:
symbol = currency["currency"]
instrument = symbol+"-PERPETUAL"
book = L2Book(instrument)
book.top_bid = [0., 0.]
book.top_ask = [0., 0.]
books[instrument] = book
request_id = await conn.get_instruments(symbol, kind="future", callback=handle_instruments)
pending[request_id] = symbol


async def get_currencies():
await conn.get_currencies(handle_currencies)


async def subscribe_books(instruments: list):
await conn.send_public(request={
"method": "public/subscribe",
"params": {
"channels": [f"book.{i}.raw" for i in instruments]
}
})


async def handle_subscription(data):
method = data.get("method")
if method and method == "subscription":
params = data["params"]
channel = params["channel"]
if channel.startswith("book."):
params_data = params["data"]
instrument = params_data["instrument_name"]
book: L2Book = books[instrument]
if "prev_change_id" in params_data:
book.handle_update(params_data)
else:
book.handle_snapshot(params_data)

if book.top_ask[0] != book.asks[0][0] or book.top_bid[0] != book.bids[0][0]:
book.top_ask = book.asks[0].copy()
book.top_bid = book.bids[0].copy()
print(f"{instrument} bid:{book.top_bid[0]} ask:{book.top_ask[0]}")
else:
print("Unknown channel", json.dumps(data))


conn.on_connect_ws = get_currencies
conn.method_routes += [("subscription", handle_subscription)]

loop = asyncio.get_event_loop()

try:
loop.run_until_complete(conn.run_receiver())
except KeyboardInterrupt:
print("Application closed by KeyboardInterrupt.")
2 changes: 1 addition & 1 deletion examples/deribit_private.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from uuid import uuid4
from typing import Pattern
from dotenv import load_dotenv
from ssc2ce.deribit import Deribit, AuthType
from ssc2ce import Deribit, AuthType


class MyApp:
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ aiohttp
python-dotenv
twine
wheel
sortedcontainers
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

setuptools.setup(
name='ssc2ce',
version="0.9.0",
version="0.10.0",
author='Oleg Nedbaylo',
author_email='olned64@gmail.com',
description='A Set of Simple Connectors for access To Cryptocurrency Exchanges',
Expand Down
1 change: 1 addition & 0 deletions ssc2ce/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .deribit import Deribit
from .common import AuthType
from .bitfinex import Bitfinex

from pkg_resources import get_distribution, DistributionNotFound
Expand Down
1 change: 1 addition & 0 deletions ssc2ce/bitfinex/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .bitfinex import Bitfinex
4 changes: 2 additions & 2 deletions ssc2ce/bitfinex.py → ssc2ce/bitfinex/bitfinex.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

import aiohttp

from .session import SessionWrapper
from .utils import resolve_route
from ssc2ce.common.session import SessionWrapper
from ssc2ce.common.utils import resolve_route

from enum import IntEnum

Expand Down
1 change: 1 addition & 0 deletions ssc2ce/common/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .auth_type import AuthType
44 changes: 44 additions & 0 deletions ssc2ce/common/abstract_l2_book.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from sortedcontainers import SortedKeyList
from abc import abstractmethod, ABC
from .l2_book_side import L2BookSide, SortedKeyList


class AbstractL2Book(ABC):
"""
"""

def __init__(self, instrument: str):
"""
:param instrument:
"""
self.instrument = instrument
self._bids = L2BookSide(is_bids=True)
self._asks = L2BookSide(is_bids=False)

@abstractmethod
def handle_snapshot(self, message: dict) -> None:
"""
:param message:
:return:
"""
pass

@abstractmethod
def handle_update(self, message: dict) -> None:
"""
:param message:
:return:
"""
pass

@property
def bids(self):
return self._bids.data

@property
def asks(self):
return self._asks.data
File renamed without changes.
8 changes: 8 additions & 0 deletions ssc2ce/common/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class Ssc2ceError(Exception):
"""Base class for errors."""


class BrokenOrderBook(Exception):
def __init__(self, instrument, prev_change_id, change_id):
self.instrument = instrument
self.message = f"instrument:{self.instrument} expected:{change_id} != received:{prev_change_id}"
63 changes: 63 additions & 0 deletions ssc2ce/common/l2_book_side.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from sortedcontainers import SortedKeyList

VERY_SMALL_NUMBER = 1e-11


class L2BookSide:

def __init__(self, is_bids: bool):
if is_bids:
self.data = SortedKeyList(key=lambda val: -val[0])
else:
self.data = SortedKeyList(key=lambda val: val[0])
self.is_bids = is_bids
self.time = None
self.changes = list()

def fill(self, source):
self.data.clear()
for item in source:
self.add(item)

def add(self, item):
price = float(item[0])
size = float(item[1])
self.changes.append([price, size, size])
self.data.add([price, size])

def update(self, price: float, size: float):
key = -price if self.is_bids else price
i = self.data.bisect_key_left(key)

if 0 <= i < len(self.data):
value = self.data[i]
else:
if size <= VERY_SMALL_NUMBER:
self.changes.append([price, size, 0.0])
return False

self.data.add([price, size])
self.changes.append([price, size, size])
return True

if size <= VERY_SMALL_NUMBER:
if value[0] == price:
old_size = self.data[i][1]
self.data.discard(value)
self.changes.append([price, size, -old_size])
return True
else:
self.changes.append([price, size, 0.0])
return False

if value[0] == price:
old_size = self.data[i][1]
self.data[i][1] = size
self.changes.append([price, size, size - old_size])
else:
self.data.add([price, size])
self.changes.append([price, size, size])
return True

def delete(self, price: float):
return self.update(price, 0.0)
File renamed without changes.
File renamed without changes.
1 change: 1 addition & 0 deletions ssc2ce/deribit/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .deribit import Deribit
15 changes: 8 additions & 7 deletions ssc2ce/deribit.py → ssc2ce/deribit/deribit.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

import aiohttp

from .exceptions import Ssc2ceError
from .common import AuthType
from .session import SessionWrapper
from .utils import resolve_route, hide_secret, IntId
from ssc2ce.common.exceptions import Ssc2ceError
from ssc2ce.common import AuthType
from ssc2ce.common.session import SessionWrapper
from ssc2ce.common.utils import resolve_route, hide_secret, IntId


class Deribit(SessionWrapper):
Expand Down Expand Up @@ -325,7 +325,7 @@ async def get_currencies(self, callback=None):
"""
return await self.send_public(request=dict(method="public/get_currencies", params={}), callback=callback)

async def get_instruments(self, currency: str, kind: str = None, callback=None) -> int:
async def get_instruments(self, currency: str, kind: str = None, expired: bool = False, callback=None) -> int:
"""
Send a request for a list available trading instruments
:param currency: The currency symbol: BTC or ETH
Expand All @@ -335,10 +335,11 @@ async def get_instruments(self, currency: str, kind: str = None, callback=None)
"""
request = {"method": "public/get_instruments",
"params": {
"currency": currency
"currency": currency,
"expired": expired
}}
if kind:
request["kind"] = kind
request["params"]["kind"] = kind

return await self.send_public(request=request, callback=callback)

Expand Down
67 changes: 67 additions & 0 deletions ssc2ce/deribit/l2_book.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import logging
from ssc2ce.common.abstract_l2_book import AbstractL2Book
from collections import deque

from ssc2ce.common.exceptions import BrokenOrderBook


class L2Book(AbstractL2Book):
change_id = None
timestamp = None
logger = logging.getLogger(__name__)

def __init__(self, instrument: str):
"""
:param instrument:
"""
AbstractL2Book.__init__(self, instrument)

def handle_snapshot(self, message: dict) -> None:
"""
:param message:
:return:
"""
self.asks.clear()
self.bids.clear()

self.change_id = message["change_id"]
self.timestamp = message["timestamp"]
for i in message['bids']:
self.bids.add([i[1], i[2]])

for i in message['asks']:
self.asks.add([i[1], i[2]])

def handle_update(self, message: dict) -> None:
"""
:param message:
:return:
"""
prev_change_id = message["prev_change_id"]
if prev_change_id != self.change_id:
raise BrokenOrderBook(self.instrument, prev_change_id, self.change_id)

self.change_id = message["change_id"]
self.timestamp = message["timestamp"]
for change in message['bids']:
if change[0] == 'new':
self._bids.add(change[1:])
elif change[0] == 'delete':
self._bids.delete(change[1])
else:
self._bids.update(price=change[1], size=change[2])

for change in message['asks']:
if change[0] == 'new':
self._asks.add(change[1:])
elif change[0] == 'delete':
self._asks.delete(change[1])
else:
self._asks.update(price=change[1], size=change[2])


def create_l2_order_book(instrument: str) -> AbstractL2Book:
return L2Book(instrument)
2 changes: 0 additions & 2 deletions ssc2ce/exceptions.py

This file was deleted.

0 comments on commit 6564aea

Please sign in to comment.