Add L1 spread streaming to kraken

bar_select
Tyler Goodlet 2020-11-06 11:35:40 -05:00
parent 043bc985df
commit be4a3df7ba
1 changed files with 129 additions and 67 deletions

View File

@ -176,8 +176,9 @@ class OHLC:
setattr(self, f, val.type(getattr(self, f))) setattr(self, f, val.type(getattr(self, f)))
async def recv_ohlc(recv): async def recv_msg(recv):
too_slow_count = last_hb = 0 too_slow_count = last_hb = 0
while True: while True:
with trio.move_on_after(1.5) as cs: with trio.move_on_after(1.5) as cs:
msg = await recv() msg = await recv()
@ -194,20 +195,50 @@ async def recv_ohlc(recv):
if isinstance(msg, dict): if isinstance(msg, dict):
if msg.get('event') == 'heartbeat': if msg.get('event') == 'heartbeat':
now = time.time() now = time.time()
delay = now - last_hb delay = now - last_hb
last_hb = now last_hb = now
log.trace(f"Heartbeat after {delay}") log.trace(f"Heartbeat after {delay}")
# TODO: hmm i guess we should use this # TODO: hmm i guess we should use this
# for determining when to do connection # for determining when to do connection
# resets eh? # resets eh?
continue continue
err = msg.get('errorMessage') err = msg.get('errorMessage')
if err: if err:
raise BrokerError(err) raise BrokerError(err)
else: else:
chan_id, ohlc_array, chan_name, pair = msg chan_id, *payload_array, chan_name, pair = msg
yield OHLC(chan_id, chan_name, pair, *ohlc_array)
if 'ohlc' in chan_name:
yield 'ohlc', OHLC(chan_id, chan_name, pair, *payload_array[0])
elif 'spread' in chan_name:
bid, ask, ts, bsize, asize = map(float, payload_array[0])
# TODO: really makes you think IB has a horrible API...
quote = {
'symbol': pair.replace('/', ''),
'ticks': [
{'type': 'bid', 'price': bid, 'size': bsize},
{'type': 'bsize', 'price': bid, 'size': bsize},
{'type': 'ask', 'price': ask, 'size': asize},
{'type': 'asize', 'price': ask, 'size': asize},
],
}
yield 'l1', quote
# elif 'book' in msg[-2]:
# chan_id, *payload_array, chan_name, pair = msg
# print(msg)
else:
print(f'UNHANDLED MSG: {msg}')
def normalize( def normalize(
@ -226,6 +257,21 @@ def normalize(
return topic, quote return topic, quote
def make_sub(pairs: List[str], data: Dict[str, Any]) -> Dict[str, str]:
"""Create a request subscription packet dict.
https://docs.kraken.com/websockets/#message-subscribe
"""
# eg. specific logic for this in kraken's sync client:
# https://github.com/krakenfx/kraken-wsclient-py/blob/master/kraken_wsclient_py/kraken_wsclient_py.py#L188
return {
'pair': pairs,
'event': 'subscribe',
'subscription': data,
}
# @tractor.msg.pub # @tractor.msg.pub
async def stream_quotes( async def stream_quotes(
# get_topics: Callable, # get_topics: Callable,
@ -247,6 +293,7 @@ async def stream_quotes(
ws_pairs = {} ws_pairs = {}
async with get_client() as client: async with get_client() as client:
# keep client cached for real-time section # keep client cached for real-time section
for sym in symbols: for sym in symbols:
ws_pairs[sym] = (await client.symbol_info(sym))['wsname'] ws_pairs[sym] = (await client.symbol_info(sym))['wsname']
@ -280,31 +327,36 @@ async def stream_quotes(
async with trio_websocket.open_websocket_url( async with trio_websocket.open_websocket_url(
'wss://ws.kraken.com', 'wss://ws.kraken.com',
) as ws: ) as ws:
# setup subs
# XXX: setup subs
# https://docs.kraken.com/websockets/#message-subscribe # https://docs.kraken.com/websockets/#message-subscribe
subs = { # specific logic for this in kraken's shitty sync client:
'pair': list(ws_pairs.values()), # https://github.com/krakenfx/kraken-wsclient-py/blob/master/kraken_wsclient_py/kraken_wsclient_py.py#L188
'event': 'subscribe', ohlc_sub = make_sub(
'subscription': { list(ws_pairs.values()),
'name': sub_type, {'name': 'ohlc', 'interval': 1}
'interval': 1, # 1 min )
# 'name': 'ticker',
# 'name': 'openOrders',
# 'depth': '25',
},
}
# TODO: we want to eventually allow unsubs which should # TODO: we want to eventually allow unsubs which should
# be completely fine to request from a separate task # be completely fine to request from a separate task
# since internally the ws methods appear to be FIFO # since internally the ws methods appear to be FIFO
# locked. # locked.
await ws.send_message(json.dumps(subs)) await ws.send_message(json.dumps(ohlc_sub))
# trade data (aka L1)
l1_sub = make_sub(
list(ws_pairs.values()),
{'name': 'spread'} # 'depth': 10}
)
await ws.send_message(json.dumps(l1_sub))
async def recv(): async def recv():
return json.loads(await ws.get_message()) return json.loads(await ws.get_message())
# pull a first quote and deliver # pull a first quote and deliver
ohlc_gen = recv_ohlc(recv) msg_gen = recv_msg(recv)
ohlc_last = await ohlc_gen.__anext__() typ, ohlc_last = await msg_gen.__anext__()
topic, quote = normalize(ohlc_last) topic, quote = normalize(ohlc_last)
@ -315,65 +367,75 @@ async def stream_quotes(
last_interval_start = ohlc_last.etime last_interval_start = ohlc_last.etime
# start streaming # start streaming
async for ohlc in ohlc_gen: async for typ, ohlc in msg_gen:
# generate tick values to match time & sales pane: if typ == 'ohlc':
# https://trade.kraken.com/charts/KRAKEN:BTC-USD?period=1m
volume = ohlc.volume
if ohlc.etime > last_interval_start: # new interval
last_interval_start = ohlc.etime
tick_volume = volume
else:
# this is the tick volume *within the interval*
tick_volume = volume - ohlc_last.volume
last = ohlc.close # TODO: can get rid of all this by using
if tick_volume: # ``trades`` subscription...
ohlc.ticks.append({
'type': 'trade',
'price': last,
'size': tick_volume,
})
topic, quote = normalize(ohlc) # generate tick values to match time & sales pane:
# https://trade.kraken.com/charts/KRAKEN:BTC-USD?period=1m
volume = ohlc.volume
# if we are the lone tick writer start writing # new interval
# the buffer with appropriate trade data if ohlc.etime > last_interval_start:
if not writer_exists: last_interval_start = ohlc.etime
# update last entry tick_volume = volume
# benchmarked in the 4-5 us range else:
o, high, low, v = shm.array[-1][ # this is the tick volume *within the interval*
['open', 'high', 'low', 'volume'] tick_volume = volume - ohlc_last.volume
]
new_v = tick_volume
if v == 0 and new_v: last = ohlc.close
# no trades for this bar yet so the open if tick_volume:
# is also the close/last trade price ohlc.ticks.append({
o = last 'type': 'trade',
'price': last,
'size': tick_volume,
})
# write shm topic, quote = normalize(ohlc)
shm.array[
['open', # if we are the lone tick writer start writing
'high', # the buffer with appropriate trade data
'low', if not writer_exists:
'close', # update last entry
'vwap', # benchmarked in the 4-5 us range
'volume'] o, high, low, v = shm.array[-1][
][-1] = ( ['open', 'high', 'low', 'volume']
o, ]
max(high, last), new_v = tick_volume
min(low, last),
last, if v == 0 and new_v:
ohlc.vwap, # no trades for this bar yet so the open
volume, # is also the close/last trade price
) o = last
# write shm
shm.array[
['open',
'high',
'low',
'close',
'vwap',
'volume']
][-1] = (
o,
max(high, last),
min(low, last),
last,
ohlc.vwap,
volume,
)
ohlc_last = ohlc
elif typ == 'l1':
quote = ohlc
topic = quote['symbol']
# XXX: format required by ``tractor.msg.pub`` # XXX: format required by ``tractor.msg.pub``
# requires a ``Dict[topic: str, quote: dict]`` # requires a ``Dict[topic: str, quote: dict]``
yield {topic: quote} yield {topic: quote}
ohlc_last = ohlc
except (ConnectionClosed, DisconnectionTimeout): except (ConnectionClosed, DisconnectionTimeout):
log.exception("Good job kraken...reconnecting") log.exception("Good job kraken...reconnecting")