Refactor cryptofeed relay api and move it to client

Added submit_limit and submit_cancel
Cache syms correctly
Lowercase search results
size_in_shm_token
Guillermo Rodriguez 2022-08-23 14:00:52 -03:00
parent d60f222bb7
commit 80a1a58bfc
No known key found for this signature in database
GPG Key ID: EC3AB66D5D83B392
3 changed files with 279 additions and 167 deletions

View File

@ -32,8 +32,8 @@ from .feed import (
stream_quotes, stream_quotes,
) )
# from .broker import ( # from .broker import (
# trades_dialogue, # trades_dialogue,
# norm_trade_records, # norm_trade_records,
# ) # )
__all__ = [ __all__ = [

View File

@ -20,9 +20,11 @@ Deribit backend.
''' '''
import json import json
import time import time
import asyncio
from contextlib import asynccontextmanager as acm, AsyncExitStack from contextlib import asynccontextmanager as acm, AsyncExitStack
from itertools import count from itertools import count
from functools import partial
from datetime import datetime from datetime import datetime
from typing import Any, List, Dict, Optional, Iterable from typing import Any, List, Dict, Optional, Iterable
@ -41,11 +43,24 @@ from .._util import resproc
from piker import config from piker import config
from piker.log import get_logger from piker.log import get_logger
from tractor.trionics import broadcast_receiver, BroadcastReceiver
from tractor import to_asyncio
from cryptofeed import FeedHandler
from cryptofeed.defines import (
DERIBIT, L1_BOOK, TRADES, OPTION, CALL, PUT
)
from cryptofeed.symbols import Symbol from cryptofeed.symbols import Symbol
log = get_logger(__name__) log = get_logger(__name__)
_spawn_kwargs = {
'infect_asyncio': True,
}
_url = 'https://www.deribit.com' _url = 'https://www.deribit.com'
_ws_url = 'wss://www.deribit.com/ws/api/v2' _ws_url = 'wss://www.deribit.com/ws/api/v2'
_testnet_ws_url = 'wss://test.deribit.com/ws/api/v2' _testnet_ws_url = 'wss://test.deribit.com/ws/api/v2'
@ -96,6 +111,8 @@ class Trade(Struct):
instrument_name: str instrument_name: str
index_price: float index_price: float
direction: str direction: str
combo_trade_id: Optional[int] = 0,
combo_id: Optional[str] = '',
amount: float amount: float
class LastTradesResult(Struct): class LastTradesResult(Struct):
@ -108,6 +125,67 @@ def deribit_timestamp(when):
return int((when.timestamp() * 1000) + (when.microsecond / 1000)) return int((when.timestamp() * 1000) + (when.microsecond / 1000))
def str_to_cb_sym(name: str) -> Symbol:
base, strike_price, expiry_date, option_type = name.split('-')
quote = base
if option_type == 'put':
option_type = PUT
elif option_type == 'call':
option_type = CALL
else:
raise Exception("Couldn\'t parse option type")
return Symbol(
base, quote,
type=OPTION,
strike_price=strike_price,
option_type=option_type,
expiry_date=expiry_date,
expiry_normalize=False)
def piker_sym_to_cb_sym(name: str) -> Symbol:
base, expiry_date, strike_price, option_type = tuple(
name.upper().split('-'))
quote = base
if option_type == 'P':
option_type = PUT
elif option_type == 'C':
option_type = CALL
else:
raise Exception("Couldn\'t parse option type")
return Symbol(
base, quote,
type=OPTION,
strike_price=strike_price,
option_type=option_type,
expiry_date=expiry_date.upper())
def cb_sym_to_deribit_inst(sym: Symbol):
# cryptofeed normalized
cb_norm = ['F', 'G', 'H', 'J', 'K', 'M', 'N', 'Q', 'U', 'V', 'X', 'Z']
# deribit specific
months = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']
exp = sym.expiry_date
# YYMDD
# 01234
year, month, day = (
exp[:2], months[cb_norm.index(exp[2:3])], exp[3:])
otype = 'C' if sym.option_type == CALL else 'P'
return f'{sym.base}-{day}{month}{year}-{sym.strike_price}-{otype}'
def get_config() -> dict[str, Any]: def get_config() -> dict[str, Any]:
conf, path = config.load() conf, path = config.load()
@ -126,7 +204,7 @@ def get_config() -> dict[str, Any]:
class Client: class Client:
def __init__(self, n: Nursery, ws: NoBsWs) -> None: def __init__(self, n: Nursery, ws: NoBsWs) -> None:
self._pairs: dict[str, Any] = {} self._pairs: dict[str, Any] = None
config = get_config().get('deribit', {}) config = get_config().get('deribit', {})
@ -148,6 +226,8 @@ class Client:
self._access_token: Optional[str] = None self._access_token: Optional[str] = None
self._refresh_token: Optional[str] = None self._refresh_token: Optional[str] = None
self.feeds = CryptoFeedRelay()
@property @property
def currencies(self): def currencies(self):
return ['btc', 'eth', 'sol', 'usd'] return ['btc', 'eth', 'sol', 'usd']
@ -298,6 +378,33 @@ class Client:
return balances return balances
async def submit_limit(
self,
symbol: str,
price: float,
action: str,
size: float
) -> dict:
"""Place an order
"""
params = {
'instrument_name': symbol.upper(),
'amount': size,
'type': 'limit',
'price': price,
}
resp = await self.json_rpc(
f'private/{action}', params)
return resp.result
async def submit_cancel(self, oid: str):
"""Send cancel request for order id
"""
resp = await self.json_rpc(
'private/cancel', {'order_id': oid})
return resp.result
async def symbol_info( async def symbol_info(
self, self,
instrument: Optional[str] = None, instrument: Optional[str] = None,
@ -308,8 +415,8 @@ class Client:
"""Get symbol info for the exchange. """Get symbol info for the exchange.
""" """
# TODO: we can load from our self._pairs cache if self._pairs:
# on repeat calls... return self._pairs
# will retrieve all symbols by default # will retrieve all symbols by default
params = { params = {
@ -322,7 +429,9 @@ class Client:
results = resp.result results = resp.result
instruments = { instruments = {
item['instrument_name']: item for item in results} item['instrument_name'].lower(): item
for item in results
}
if instrument is not None: if instrument is not None:
return instruments[instrument] return instruments[instrument]
@ -342,9 +451,6 @@ class Client:
pattern: str, pattern: str,
limit: int = 30, limit: int = 30,
) -> dict[str, Any]: ) -> dict[str, Any]:
if self._pairs is not None:
data = self._pairs
else:
data = await self.symbol_info() data = await self.symbol_info()
matches = fuzzy.extractBests( matches = fuzzy.extractBests(
@ -354,7 +460,7 @@ class Client:
limit=limit limit=limit
) )
# repack in dict form # repack in dict form
return {item[0]['instrument_name']: item[0] return {item[0]['instrument_name'].lower(): item[0]
for item in matches} for item in matches}
async def bars( async def bars(
@ -437,3 +543,141 @@ async def get_client() -> Client:
await client.start_rpc() await client.start_rpc()
await client.cache_symbols() await client.cache_symbols()
yield client yield client
await client.feeds.stop()
class CryptoFeedRelay:
def __init__(self):
self._fh = FeedHandler(config=get_config())
self._price_streams: dict[str, BroadcastReceiver] = {}
self._order_stream: Optional[BroadcastReceiver] = None
self._loop = None
async def stop(self):
await to_asyncio.run_task(
partial(self._fh.stop_async, loop=self._loop))
@acm
async def open_price_feed(
self,
instruments: List[str]
) -> trio.abc.ReceiveStream:
inst_str = ','.join(instruments)
instruments = [piker_sym_to_cb_sym(i) for i in instruments]
if inst_str in self._price_streams:
# TODO: a good value for maxlen?
yield broadcast_receiver(self._price_streams[inst_str], 10)
else:
async def relay(
from_trio: asyncio.Queue,
to_trio: trio.abc.SendChannel,
) -> None:
async def _trade(data: dict, receipt_timestamp):
to_trio.send_nowait(('trade', {
'symbol': cb_sym_to_deribit_inst(
str_to_cb_sym(data.symbol)).lower(),
'last': data,
'broker_ts': time.time(),
'data': data.to_dict(),
'receipt': receipt_timestamp
}))
async def _l1(data: dict, receipt_timestamp):
to_trio.send_nowait(('l1', {
'symbol': cb_sym_to_deribit_inst(
str_to_cb_sym(data.symbol)).lower(),
'ticks': [
{'type': 'bid',
'price': float(data.bid_price), 'size': float(data.bid_size)},
{'type': 'bsize',
'price': float(data.bid_price), 'size': float(data.bid_size)},
{'type': 'ask',
'price': float(data.ask_price), 'size': float(data.ask_size)},
{'type': 'asize',
'price': float(data.ask_price), 'size': float(data.ask_size)}
]
}))
self._fh.add_feed(
DERIBIT,
channels=[TRADES, L1_BOOK],
symbols=instruments,
callbacks={
TRADES: _trade,
L1_BOOK: _l1
})
if not self._fh.running:
self._fh.run(start_loop=False)
self._loop = asyncio.get_event_loop()
# sync with trio
to_trio.send_nowait(None)
try:
await asyncio.sleep(float('inf'))
except asyncio.exceptions.CancelledError:
...
async with to_asyncio.open_channel_from(
relay
) as (first, chan):
self._price_streams[inst_str] = chan
yield self._price_streams[inst_str]
@acm
async def open_order_feed(
self,
instruments: List[str]
) -> trio.abc.ReceiveStream:
inst_str = ','.join(instruments)
instruments = [piker_sym_to_cb_sym(i) for i in instruments]
if self._order_stream:
yield broadcast_receiver(self._order_streams[inst_str], 10)
else:
async def relay(
from_trio: asyncio.Queue,
to_trio: trio.abc.SendChannel,
) -> None:
async def _fill(data: dict, receipt_timestamp):
breakpoint()
async def _order_info(data: dict, receipt_timestamp):
breakpoint()
self._fh.add_feed(
DERIBIT,
channels=[FILLS, ORDER_INFO],
symbols=instruments,
callbacks={
FILLS: _fill,
ORDER_INFO: _order_info,
})
if not self._fh.running:
self._fh.run(start_loop=False)
self._loop = asyncio.get_event_loop()
# sync with trio
to_trio.send_nowait(None)
try:
await asyncio.sleep(float('inf'))
except asyncio.exceptions.CancelledError:
...
async with to_asyncio.open_channel_from(
relay
) as (first, chan):
self._order_stream = chan
yield self._order_stream

View File

@ -18,9 +18,6 @@
Deribit backend. Deribit backend.
''' '''
import asyncio
from async_generator import aclosing
from contextlib import asynccontextmanager as acm from contextlib import asynccontextmanager as acm
from datetime import datetime from datetime import datetime
from typing import Any, Optional, List, Callable from typing import Any, Optional, List, Callable
@ -32,7 +29,6 @@ import pendulum
from fuzzywuzzy import process as fuzzy from fuzzywuzzy import process as fuzzy
import numpy as np import numpy as np
import tractor import tractor
from tractor import to_asyncio
from piker._cacheables import open_cached_client from piker._cacheables import open_cached_client
from piker.log import get_logger, get_console_log from piker.log import get_logger, get_console_log
@ -49,7 +45,11 @@ from cryptofeed.defines import (
) )
from cryptofeed.symbols import Symbol from cryptofeed.symbols import Symbol
from .api import Client, Trade, get_config from .api import (
Client, Trade,
get_config,
str_to_cb_sym, piker_sym_to_cb_sym, cb_sym_to_deribit_inst
)
_spawn_kwargs = { _spawn_kwargs = {
'infect_asyncio': True, 'infect_asyncio': True,
@ -59,145 +59,6 @@ _spawn_kwargs = {
log = get_logger(__name__) log = get_logger(__name__)
_url = 'https://www.deribit.com'
def str_to_cb_sym(name: str) -> Symbol:
base, strike_price, expiry_date, option_type = name.split('-')
quote = base
if option_type == 'put':
option_type = PUT
elif option_type == 'call':
option_type = CALL
else:
raise BaseException("Couldn\'t parse option type")
return Symbol(
base, quote,
type=OPTION,
strike_price=strike_price,
option_type=option_type,
expiry_date=expiry_date,
expiry_normalize=False)
def piker_sym_to_cb_sym(name: str) -> Symbol:
base, expiry_date, strike_price, option_type = tuple(
name.upper().split('-'))
quote = base
if option_type == 'P':
option_type = PUT
elif option_type == 'C':
option_type = CALL
else:
raise BaseException("Couldn\'t parse option type")
return Symbol(
base, quote,
type=OPTION,
strike_price=strike_price,
option_type=option_type,
expiry_date=expiry_date.upper())
def cb_sym_to_deribit_inst(sym: Symbol):
# cryptofeed normalized
cb_norm = ['F', 'G', 'H', 'J', 'K', 'M', 'N', 'Q', 'U', 'V', 'X', 'Z']
# deribit specific
months = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']
exp = sym.expiry_date
# YYMDD
# 01234
year, month, day = (
exp[:2], months[cb_norm.index(exp[2:3])], exp[3:])
otype = 'C' if sym.option_type == CALL else 'P'
return f'{sym.base}-{day}{month}{year}-{sym.strike_price}-{otype}'
# inside here we are in an asyncio context
async def open_aio_cryptofeed_relay(
from_trio: asyncio.Queue,
to_trio: trio.abc.SendChannel,
instruments: List[str] = []
) -> None:
instruments = [piker_sym_to_cb_sym(i) for i in instruments]
async def trade_cb(data: dict, receipt_timestamp):
to_trio.send_nowait(('trade', {
'symbol': cb_sym_to_deribit_inst(
str_to_cb_sym(data.symbol)).lower(),
'last': data,
'broker_ts': time.time(),
'data': data.to_dict(),
'receipt': receipt_timestamp
}))
async def l1_book_cb(data: dict, receipt_timestamp):
to_trio.send_nowait(('l1', {
'symbol': cb_sym_to_deribit_inst(
str_to_cb_sym(data.symbol)).lower(),
'ticks': [
{'type': 'bid',
'price': float(data.bid_price), 'size': float(data.bid_size)},
{'type': 'bsize',
'price': float(data.bid_price), 'size': float(data.bid_size)},
{'type': 'ask',
'price': float(data.ask_price), 'size': float(data.ask_size)},
{'type': 'asize',
'price': float(data.ask_price), 'size': float(data.ask_size)}
]
}))
fh = FeedHandler(config=get_config())
fh.run(start_loop=False)
fh.add_feed(
DERIBIT,
channels=[L1_BOOK],
symbols=instruments,
callbacks={L1_BOOK: l1_book_cb})
fh.add_feed(
DERIBIT,
channels=[TRADES],
symbols=instruments,
callbacks={TRADES: trade_cb})
# sync with trio
to_trio.send_nowait(None)
try:
await asyncio.sleep(float('inf'))
except asyncio.exceptions.CancelledError:
...
@acm
async def open_cryptofeeds(
instruments: List[str]
) -> trio.abc.ReceiveStream:
async with to_asyncio.open_channel_from(
open_aio_cryptofeed_relay,
instruments=instruments,
) as (first, chan):
yield chan
@acm @acm
async def open_history_client( async def open_history_client(
instrument: str, instrument: str,
@ -265,8 +126,7 @@ async def stream_quotes(
async with ( async with (
open_cached_client('deribit') as client, open_cached_client('deribit') as client,
send_chan as send_chan, send_chan as send_chan
open_cryptofeeds(symbols) as stream
): ):
init_msgs = { init_msgs = {
@ -284,20 +144,23 @@ async def stream_quotes(
nsym = piker_sym_to_cb_sym(sym) nsym = piker_sym_to_cb_sym(sym)
# keep client cached for real-time section async with client.feeds.open_price_feed(
symbols) as stream:
cache = await client.cache_symbols() cache = await client.cache_symbols()
async with aclosing(stream):
last_trades = (await client.last_trades( last_trades = (await client.last_trades(
cb_sym_to_deribit_inst(nsym), count=1)).trades cb_sym_to_deribit_inst(nsym), count=1)).trades
if len(last_trades) == 0: if len(last_trades) == 0:
async for typ, quote in stream: last_trade = None
while not last_trade:
typ, quote = await stream.receive()
if typ == 'trade': if typ == 'trade':
last_trade = Trade(**quote['data']) last_trade = Trade(**(quote['data']))
else: else:
last_trade = Trade(**last_trades[0]) last_trade = Trade(**(last_trades[0]))
first_quote = { first_quote = {
'symbol': sym, 'symbol': sym,
@ -314,10 +177,15 @@ async def stream_quotes(
feed_is_live.set() feed_is_live.set()
async for typ, quote in stream: try:
while True:
typ, quote = await stream.receive()
topic = quote['symbol'] topic = quote['symbol']
await send_chan.send({topic: quote}) await send_chan.send({topic: quote})
except trio.ClosedResourceError:
...
@tractor.context @tractor.context
async def open_symbol_search( async def open_symbol_search(