Finally get a chart going! lots of fixes to streaming machinery and custom cryptofeed fork with fixes

deribit
Guillermo Rodriguez 2022-06-26 22:38:23 -03:00
parent e558e5837e
commit 28e025d02e
No known key found for this signature in database
GPG Key ID: EC3AB66D5D83B392
3 changed files with 173 additions and 76 deletions

View File

@ -19,6 +19,7 @@ Deribit backend
""" """
import asyncio 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 ( from typing import (
@ -74,7 +75,7 @@ def get_config() -> dict[str, Any]:
return {} return {}
conf['log'] = {} conf['log'] = {}
conf['log']['filename'] = 'feedhandler.log' conf['log']['filename'] = '/tmp/feedhandler.log'
conf['log']['level'] = 'WARNING' conf['log']['level'] = 'WARNING'
return conf return conf
@ -95,9 +96,19 @@ _ohlc_dtype = [
('low', float), ('low', float),
('close', float), ('close', float),
('volume', float), ('volume', float),
('bar_wap', float), # will be zeroed by sampler if not filled # ('bar_wap', float), # will be zeroed by sampler if not filled
] ]
class JSONRPCResult(BaseModel):
jsonrpc: str = '2.0'
result: dict
usIn: int
usOut: int
usDiff: int
testnet: bool
class KLinesResult(BaseModel): class KLinesResult(BaseModel):
close: List[float] close: List[float]
cost: List[float] cost: List[float]
@ -108,13 +119,30 @@ class KLinesResult(BaseModel):
ticks: List[int] ticks: List[int]
volume: List[float] volume: List[float]
class KLines(BaseModel):
jsonrpc: str = '2.0' class KLines(JSONRPCResult):
result: KLinesResult result: KLinesResult
usIn: int
usOut: int
usDiff: int class Trade(BaseModel):
testnet: bool trade_seq: int
trade_id: str
timestamp: int
tick_direction: int
price: float
mark_price: float
iv: float
instrument_name: str
index_price: float
direction: str
amount: float
class LastTradesResult(BaseModel):
trades: List[Trade]
has_more: bool
class LastTrades(JSONRPCResult):
result: LastTradesResult
# convert datetime obj timestamp to unixtime in milliseconds # convert datetime obj timestamp to unixtime in milliseconds
@ -122,6 +150,68 @@ 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 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}'
class Client: class Client:
def __init__(self) -> None: def __init__(self) -> None:
@ -255,10 +345,24 @@ class Client:
new_bars.append((i,) + tuple(row)) new_bars.append((i,) + tuple(row))
array = np.array( array = np.array(new_bars, dtype=_ohlc_dtype) if as_np else klines
[i, ], dtype=_ohlc_dtype) if as_np else klines
return array return array
async def last_trades(
self,
instrument: str,
count: int = 10
):
response = await self._api(
'get_last_trades_by_instrument',
params={
'instrument_name': instrument,
'count': count
}
)
return LastTrades(**response)
@acm @acm
async def get_client() -> Client: async def get_client() -> Client:
@ -274,42 +378,22 @@ async def open_aio_cryptofeed_relay(
instruments: List[str] = [] instruments: List[str] = []
) -> None: ) -> None:
conf = get_config() instruments = [piker_sym_to_cb_sym(i) for i in instruments]
def format_sym(name: str) -> str:
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("Instrument name must end in 'c' for calls or 'p' for puts")
return Symbol(
base, quote,
type=OPTION,
strike_price=strike_price,
option_type=option_type,
expiry_date=expiry_date.upper()).normalized
instruments = [format_sym(i) for i in instruments]
async def trade_cb(data: dict, receipt_timestamp): async def trade_cb(data: dict, receipt_timestamp):
breakpoint() to_trio.send_nowait(('trade', {
# to_trio.send_nowait(('trade', { 'symbol': cb_sym_to_deribit_inst(
# 'symbol': data.symbol.lower(), str_to_cb_sym(data.symbol)).lower(),
# 'last': data. 'last': data,
# 'broker_ts': time.time(), 'broker_ts': time.time(),
# 'data': data.to_dict(), 'data': data.to_dict(),
# 'receipt': receipt_timestamp})) 'receipt': receipt_timestamp
}))
async def l1_book_cb(data: dict, receipt_timestamp): async def l1_book_cb(data: dict, receipt_timestamp):
to_trio.send_nowait(('l1', { to_trio.send_nowait(('l1', {
'symbol': data.symbol.lower(), 'symbol': cb_sym_to_deribit_inst(
str_to_cb_sym(data.symbol)).lower(),
'ticks': [ 'ticks': [
{'type': 'bid', {'type': 'bid',
'price': float(data.bid_price), 'size': float(data.bid_size)}, 'price': float(data.bid_price), 'size': float(data.bid_size)},
@ -319,45 +403,42 @@ async def open_aio_cryptofeed_relay(
'price': float(data.ask_price), 'size': float(data.ask_size)}, 'price': float(data.ask_price), 'size': float(data.ask_size)},
{'type': 'asize', {'type': 'asize',
'price': float(data.ask_price), 'size': float(data.ask_size)} 'price': float(data.ask_price), 'size': float(data.ask_size)}
]})) ]
}))
fh = FeedHandler(config=conf) fh = FeedHandler(config=get_config())
fh.run(start_loop=False) fh.run(start_loop=False)
fh.add_feed( fh.add_feed(
DERIBIT, DERIBIT,
channels=[L1_BOOK, TRADES], channels=[L1_BOOK],
symbols=instruments, symbols=instruments,
callbacks={ callbacks={L1_BOOK: l1_book_cb})
L1_BOOK: L1BookCallback(l1_book_cb),
TRADES: TradeCallback(trade_cb) fh.add_feed(
}) DERIBIT,
channels=[TRADES],
symbols=instruments,
callbacks={TRADES: trade_cb})
# sync with trio # sync with trio
to_trio.send_nowait(None) to_trio.send_nowait(None)
await from_trio.get() await asyncio.sleep(float('inf'))
@acm
async def open_cryptofeeds( async def open_cryptofeeds(
instruments: List[str],
to_chart: trio.abc.SendChannel,
# startup sync instruments: List[str]
task_status: TaskStatus[tuple[dict, dict]] = trio.TASK_STATUS_IGNORED,
): ) -> trio.abc.ReceiveStream:
async with to_asyncio.open_channel_from( async with to_asyncio.open_channel_from(
open_aio_cryptofeed_relay, open_aio_cryptofeed_relay,
instruments=instruments, instruments=instruments,
) as (first, chan): ) as (first, chan):
assert first is None yield chan
await chan.send(None)
async with chan.subscribe() as msg_stream:
task_status.started()
async for msg in msg_stream:
await to_chart.send(msg)
@acm @acm
@ -420,40 +501,53 @@ async def stream_quotes(
get_console_log(loglevel or tractor.current_actor().loglevel) get_console_log(loglevel or tractor.current_actor().loglevel)
sym = symbols[0] sym = symbols[0]
to_chart, from_feed = trio.open_memory_channel(1)
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,
trio.open_nursery() as n trio.open_nursery() as n,
open_cryptofeeds(symbols) as stream
): ):
await n.start(
open_cryptofeeds, symbols, to_chart)
init_msgs = { init_msgs = {
# pass back token, and bool, signalling if we're the writer # pass back token, and bool, signalling if we're the writer
# and that history has been written # and that history has been written
sym: { sym: {
'symbol_info': {}, 'symbol_info': {
'asset_type': 'option'
},
'shm_write_opts': {'sum_tick_vml': False}, 'shm_write_opts': {'sum_tick_vml': False},
'fqsn': sym, 'fqsn': sym,
}, },
} }
nsym = piker_sym_to_cb_sym(sym)
# keep client cached for real-time section # keep client cached for real-time section
cache = await client.cache_symbols() cache = await client.cache_symbols()
async with from_feed: last_trade = (await client.last_trades(
typ, quote = await anext(from_feed) cb_sym_to_deribit_inst(nsym), count=1)).result.trades[0]
while typ != 'trade': first_quote = {
typ, quote = await anext(from_feed) 'symbol': sym,
'last': last_trade.price,
'brokerd_ts': last_trade.timestamp,
'ticks': [{
'type': 'trade',
'price': last_trade.price,
'size': last_trade.amount,
'broker_ts': last_trade.timestamp
}]
}
task_status.started((init_msgs, first_quote))
task_status.started((init_msgs, quote)) async with aclosing(stream):
feed_is_live.set()
async for typ, msg in from_feed: async for typ, quote in stream:
topic = msg['symbol'] topic = quote['symbol']
await send_chan.send({topic: msg}) await send_chan.send({topic: quote})
@tractor.context @tractor.context

View File

@ -18,3 +18,6 @@
# ``asyncvnc`` for sending interactions to ib-gw inside docker # ``asyncvnc`` for sending interactions to ib-gw inside docker
-e git+https://github.com/pikers/asyncvnc.git@main#egg=asyncvnc -e git+https://github.com/pikers/asyncvnc.git@main#egg=asyncvnc
# ``cryptofeed`` for connecting to various crypto exchanges + custom fixes
-e git+https://github.com/guilledk/cryptofeed.git@date_parsing#egg=cryptofeed

View File

@ -58,11 +58,11 @@ setup(
# 'trimeter', # not released yet.. # 'trimeter', # not released yet..
# 'tractor', # 'tractor',
# asyncvnc, # asyncvnc,
# 'cryptofeed',
# brokers # brokers
'asks==2.4.8', 'asks==2.4.8',
'ib_insync', 'ib_insync',
'cryptofeed',
# numerics # numerics
'pendulum', # easier datetimes 'pendulum', # easier datetimes