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