Add real-time incremental pp updates

Moves to using the new `piker.pp` apis to both store real-time trade
events in a ledger file as well emit position update msgs (which were
not in this backend at all prior) when new orders clear (aka fill).

In terms of outstanding issues,
- solves the pp update part of the bugs reported in #310
- starts a msg case block in prep for #293

Details of rework:
- move the `subscribe()` ws fixture to module level and `partial()` in
  the client token instead of passing it to the instance; in prep for
  removal of the `.token` attr from the `NoBsWs` wrapper.
- drop `make_auth_sub()` since it was too thin and we can just
  do it all succinctly in `subscribe()`
- filter trade update msgs to those not yet stored int the toml ledger
- much better kraken api msg unpacking using new `match:` synax B)

Resolves #311
notokeninwswrapper
Tyler Goodlet 2022-07-03 14:39:33 -04:00
parent 214f864dcf
commit 7846446a44
1 changed files with 145 additions and 86 deletions

View File

@ -19,6 +19,8 @@ Order api and machinery
''' '''
from contextlib import asynccontextmanager as acm from contextlib import asynccontextmanager as acm
from functools import partial
from itertools import chain
from pprint import pformat from pprint import pformat
import time import time
from typing import ( from typing import (
@ -234,20 +236,49 @@ async def handle_order_requests(
log.error(f'Unknown order command: {request_msg}') log.error(f'Unknown order command: {request_msg}')
def make_auth_sub(data: dict[str, Any]) -> dict[str, str]: @acm
async def subscribe(
ws: wsproto.WSConnection,
token: str,
subs: list[str] = ['ownTrades', 'openOrders'],
):
''' '''
Create a request subscription packet dict. Setup ws api subscriptions:
## TODO: point to the auth urls
https://docs.kraken.com/websockets/#message-subscribe https://docs.kraken.com/websockets/#message-subscribe
By default we sign up for trade and order update events.
''' '''
# eg. specific logic for this in kraken's sync client: # more 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 # https://github.com/krakenfx/kraken-wsclient-py/blob/master/kraken_wsclient_py/kraken_wsclient_py.py#L188
return {
'event': 'subscribe', assert token
'subscription': data, for sub in subs:
} msg = {
'event': 'subscribe',
'subscription': {
'name': sub,
'token': token,
}
}
# 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_msg(msg)
yield
for sub in subs:
# unsub from all pairs on teardown
await ws.send_msg({
'event': 'unsubscribe',
'subscription': [sub],
})
# XXX: do we need to ack the unsub?
# await ws.recv_msg()
@tractor.context @tractor.context
@ -259,33 +290,6 @@ async def trades_dialogue(
# XXX: required to propagate ``tractor`` loglevel to piker logging # XXX: required to propagate ``tractor`` loglevel to piker logging
get_console_log(loglevel or tractor.current_actor().loglevel) get_console_log(loglevel or tractor.current_actor().loglevel)
@acm
async def subscribe(ws: wsproto.WSConnection, token: str):
# XXX: setup subs
# https://docs.kraken.com/websockets/#message-subscribe
# 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
trades_sub = make_auth_sub(
{'name': 'ownTrades', 'token': token}
)
# 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_msg(trades_sub)
yield
# unsub from all pairs on teardown
await ws.send_msg({
'event': 'unsubscribe',
'subscription': ['ownTrades'],
})
# XXX: do we need to ack the unsub?
# await ws.recv_msg()
async with get_client() as client: async with get_client() as client:
# TODO: make ems flip to paper mode via # TODO: make ems flip to paper mode via
@ -347,8 +351,10 @@ async def trades_dialogue(
ctx.open_stream() as ems_stream, ctx.open_stream() as ems_stream,
open_autorecon_ws( open_autorecon_ws(
'wss://ws-auth.kraken.com/', 'wss://ws-auth.kraken.com/',
fixture=subscribe, fixture=partial(
token=token, subscribe,
token=token,
),
) as ws, ) as ws,
trio.open_nursery() as n, trio.open_nursery() as n,
): ):
@ -356,7 +362,6 @@ async def trades_dialogue(
n.start_soon(handle_order_requests, client, ems_stream) n.start_soon(handle_order_requests, client, ems_stream)
count: int = 0 count: int = 0
ledger_txids = {r.tid for r in trans}
# process and relay trades events to ems # process and relay trades events to ems
# https://docs.kraken.com/websockets/#message-ownTrades # https://docs.kraken.com/websockets/#message-ownTrades
@ -367,62 +372,116 @@ async def trades_dialogue(
'ownTrades', 'ownTrades',
{'sequence': seq}, {'sequence': seq},
]: ]:
# ensure that we are only processing new trades # XXX: do we actually need this orrr?
# ensure that we are only processing new trades?
assert seq > count assert seq > count
count += 1 count += 1
for entries in trades_msgs: # flatten msgs for processing
for tid, msg in entries.items(): trades = {
tid: trade
for entry in trades_msgs
for (tid, trade) in entry.items()
if tid in ledger_txids: # only emit entries which are already not-in-ledger
log.debug(f'Skipping ledgered {tid}:{msg}') if tid not in {r.tid for r in trans}
continue }
for tid, trade in trades.items():
# yield trade # parse-cast
reqid = msg['ordertxid'] reqid = trade['ordertxid']
action = msg['type'] action = trade['type']
price = float(msg['price']) price = float(trade['price'])
size = float(msg['vol']) size = float(trade['vol'])
broker_time = float(msg['time']) broker_time = float(trade['time'])
# send a fill msg for gui update # send a fill msg for gui update
fill_msg = BrokerdFill( fill_msg = BrokerdFill(
reqid=reqid, reqid=reqid,
time_ns=time.time_ns(), time_ns=time.time_ns(),
action=action, action=action,
size=size, size=size,
price=price, price=price,
# TODO: maybe capture more msg data # TODO: maybe capture more msg data
# i.e fees? # i.e fees?
broker_details={'name': 'kraken'}, broker_details={'name': 'kraken'},
broker_time=broker_time broker_time=broker_time
) )
await ems_stream.send(fill_msg.dict())
await ems_stream.send(fill_msg.dict()) filled_msg = BrokerdStatus(
filled_msg = BrokerdStatus( reqid=reqid,
reqid=reqid, time_ns=time.time_ns(),
time_ns=time.time_ns(),
account=acc_name, account=acc_name,
status='filled', status='filled',
filled=size, filled=size,
reason='Order filled by kraken', reason='Order filled by kraken',
broker_details={ broker_details={
'name': 'kraken', 'name': 'kraken',
'broker_time': broker_time 'broker_time': broker_time
}, },
# TODO: figure out if kraken gives a count # TODO: figure out if kraken gives a count
# of how many units of underlying were # of how many units of underlying were
# filled. Alternatively we can decrement # filled. Alternatively we can decrement
# this value ourselves by associating and # this value ourselves by associating and
# calcing from the diff with the original # calcing from the diff with the original
# client-side request, see: # client-side request, see:
# https://github.com/pikers/piker/issues/296 # https://github.com/pikers/piker/issues/296
remaining=0, remaining=0,
) )
await ems_stream.send(filled_msg.dict()) await ems_stream.send(filled_msg.dict())
# update ledger and position tracking
trans = await update_ledger(acctid, trades)
active, closed = pp.update_pps_conf(
'kraken',
acctid,
trade_records=trans,
ledger_reload={}.fromkeys(
t.bsuid for t in trans),
)
# emit pp msgs
for pos in filter(
bool,
chain(active.values(), closed.values()),
):
pp_msg = BrokerdPosition(
broker='kraken',
# XXX: ok so this is annoying, we're
# relaying an account name with the
# backend suffix prefixed but when
# reading accounts from ledgers we
# don't need it and/or it's prefixed
# in the section table.. we should
# just strip this from the message
# right since `.broker` is already
# included?
account=f'kraken.{acctid}',
symbol=pos.symbol.front_fqsn(),
size=pos.size,
avg_price=pos.be_price,
# TODO
# currency=''
)
await ems_stream.send(pp_msg.dict())
case [
trades_msgs,
'openOrders',
{'sequence': seq},
]:
# TODO: async order update handling which we
# should remove from `handle_order_requests()`
# above:
# https://github.com/pikers/piker/issues/293
# https://github.com/pikers/piker/issues/310
log.info(f'Order update {seq}:{trades_msgs}')
case _: case _:
log.warning(f'Unhandled trades msg: {msg}') log.warning(f'Unhandled trades msg: {msg}')
@ -452,7 +511,7 @@ def norm_trade_records(
size=float(size), size=float(size),
price=float(record['price']), price=float(record['price']),
cost=float(record['fee']), cost=float(record['fee']),
dt=pendulum.from_timestamp(record['time']), dt=pendulum.from_timestamp(float(record['time'])),
bsuid=bsuid, bsuid=bsuid,
# XXX: there are no derivs on kraken right? # XXX: there are no derivs on kraken right?