kraken: handle ws live trading API symbology

Of course I missed this first try but, we need to use the ws market pair
symbology set (since apparently kraken loves redundancy at least 3 times
XD) when processing transactions that arrive from live clears since it's
an entirely different `LTC/EUR` style key then the `XLTCEUR` style
delivered from the ReST eps..

As part of this:
- add `Client._altnames`, `._wsnames` as `dict[str, Pair]` tables,
  leaving the `._AssetPairs` table as is keyed by the "xname"s.
- Change `Pair.respname: str` -> `.xname` since these keys all just seem
  to have a weird 'X' prefix.
- do the appropriately keyed pair table lookup via a new `api_name_set:
  str` to `norm_trade_records()` and set is correctly in the ws live txn
  handler task.
ib_py311_fixes
Tyler Goodlet 2023-08-30 16:09:45 -04:00
parent 778d26067d
commit 481618cc51
5 changed files with 81 additions and 47 deletions

View File

@ -106,16 +106,19 @@ class InvalidKey(ValueError):
class Client:
# symbol mapping from all names to the altname
_altnames: dict[str, str] = {}
# key-ed by kraken's own bs_mktids (like fricking "XXMRZEUR")
# with said keys used directly from EP responses so that ledger
# parsing can be easily accomplished from both trade-event-msgs
# and offline toml files
# assets and mkt pairs are key-ed by kraken's ReST response
# symbol-bs_mktids (we call them "X-keys" like fricking
# "XXMRZEUR"). these keys used directly since ledger endpoints
# return transaction sets keyed with the same set!
_Assets: dict[str, Asset] = {}
_AssetPairs: dict[str, Pair] = {}
# offer lookup tables for all .altname and .wsname
# to the equivalent .xname so that various symbol-schemas
# can be mapped to `Pair`s in the tables above.
_altnames: dict[str, str] = {}
_wsnames: dict[str, str] = {}
# key-ed by `Pair.bs_fqme: str`, and thus used for search
# allowing for lookup using piker's own FQME symbology sys.
_pairs: dict[str, Pair] = {}
@ -209,8 +212,8 @@ class Client:
by_bsmktid: dict[str, dict] = resp['result']
balances: dict = {}
for respname, bal in by_bsmktid.items():
asset: Asset = self._Assets[respname]
for xname, bal in by_bsmktid.items():
asset: Asset = self._Assets[xname]
# TODO: which KEY should we use? it's used to index
# the `Account.pps: dict` ..
@ -367,7 +370,6 @@ class Client:
asset_key: str = entry['asset']
asset: Asset = self._Assets[asset_key]
asset_key: str = asset.name.lower()
# asset_key: str = self._altnames[asset_key].lower()
# XXX: this is in the asset units (likely) so it isn't
# quite the same as a commisions cost necessarily..)
@ -473,25 +475,31 @@ class Client:
if err:
raise SymbolNotFound(pair_patt)
# NOTE: we key pairs by our custom defined `.bs_fqme`
# field since we want to offer search over this key
# set, callers should fill out lookup tables for
# kraken's bs_mktid keys to map to these keys!
for key, data in resp['result'].items():
pair = Pair(respname=key, **data)
# NOTE: we try to key pairs by our custom defined
# `.bs_fqme` field since we want to offer search over
# this pattern set, callers should fill out lookup
# tables for kraken's bs_mktid keys to map to these
# keys!
# XXX: FURTHER kraken's data eng team decided to offer
# 3 frickin market-pair-symbol key sets depending on
# which frickin API is being used.
# Example for the trading pair 'LTC<EUR'
# - the "X-key" from rest eps 'XLTCZEUR'
# - the "websocket key" from ws msgs is 'LTC/EUR'
# - the "altname key" also delivered in pair info is 'LTCEUR'
for xkey, data in resp['result'].items():
# always cache so we can possibly do faster lookup
self._AssetPairs[key] = pair
# NOTE: always cache in pairs tables for faster lookup
pair = Pair(xname=xkey, **data)
bs_fqme: str = pair.bs_fqme
self._pairs[bs_fqme] = pair
# register the piker pair under all monikers, a giant flat
# surjection of all possible (and stupid) kraken names to
# the FMQE style piker key.
self._altnames[pair.altname] = bs_fqme
self._altnames[pair.wsname] = bs_fqme
# register the above `Pair` structs for all
# key-sets/monikers: a set of 4 (frickin) tables
# acting as a combined surjection of all possible
# (and stupid) kraken names to their `Pair` obj.
self._AssetPairs[xkey] = pair
self._pairs[pair.bs_fqme] = pair
self._altnames[pair.altname] = pair
self._wsnames[pair.wsname] = pair
if pair_patt is not None:
return next(iter(self._pairs.items()))[1]
@ -506,12 +514,13 @@ class Client:
Load all market pair info build and cache it for downstream
use.
An ``._altnames: dict[str, str]`` is available for looking
up the piker-native FQME style `Pair.bs_fqme: str` for any
input of the three (yes, it's that idiotic) available
key-sets that kraken frickin offers depending on the API
including the .altname, .wsname and the weird ass default
set they return in rest responses..
Multiple pair info lookup tables (like ``._altnames:
dict[str, str]``) are created for looking up the
piker-native `Pair`-struct from any input of the three
(yes, it's that idiotic..) available symbol/pair-key-sets
that kraken frickin offers depending on the API including
the .altname, .wsname and the weird ass default set they
return in ReST responses .xname..
'''
if (
@ -628,7 +637,7 @@ class Client:
def to_bs_fqme(
cls,
pair_str: str
) -> tuple[str, Pair]:
) -> str:
'''
Normalize symbol names to to a 3x3 pair from the global
definition map which we build out from the data retreived from
@ -636,7 +645,7 @@ class Client:
'''
try:
return cls._altnames[pair_str.upper()]
return cls._altnames[pair_str.upper()].bs_fqme
except KeyError as ke:
raise SymbolNotFound(f'kraken has no {ke.args[0]}')

View File

@ -513,6 +513,7 @@ async def open_trade_dialog(
ledger_trans: dict[str, Transaction] = await norm_trade_records(
ledger,
client,
api_name_set='xname',
)
if not acnt.pps:
@ -534,6 +535,7 @@ async def open_trade_dialog(
api_trans: dict[str, Transaction] = await norm_trade_records(
tids2trades,
client,
api_name_set='xname',
)
# retrieve kraken reported balances
@ -743,6 +745,7 @@ async def handle_order_updates(
new_trans = await norm_trade_records(
trades,
client,
api_name_set='wsname',
)
ppmsgs = trades2pps(
acnt,

View File

@ -64,9 +64,19 @@ def norm_trade(
'sell': -1,
}[record['type']]
rest_pair_key: str = record['pair']
pair: Pair = pairs[rest_pair_key]
# NOTE: this value may be either the websocket OR the rest schema
# so we need to detect the key format and then choose the
# correct symbol lookup table to evetually get a ``Pair``..
# See internals of `Client.asset_pairs()` for deats!
src_pair_key: str = record['pair']
# XXX: kraken's data engineering is soo bad they require THREE
# different pair schemas (more or less seemingly tied to
# transport-APIs)..LITERALLY they return different market id
# pairs in the ledger endpoints vs. the websocket event subs..
# lookup pair using appropriately provided tabled depending
# on API-key-schema..
pair: Pair = pairs[src_pair_key]
fqme: str = pair.bs_fqme.lower() + '.kraken'
return Transaction(
@ -83,6 +93,7 @@ def norm_trade(
async def norm_trade_records(
ledger: dict[str, Any],
client: Client,
api_name_set: str = 'xname',
) -> dict[str, Transaction]:
'''
@ -97,11 +108,16 @@ async def norm_trade_records(
# mkt: MktPair = (await get_mkt_info(manual_fqme))[0]
# fqme: str = mkt.fqme
# assert fqme == manual_fqme
pairs: dict[str, Pair] = {
'xname': client._AssetPairs,
'wsname': client._wsnames,
'altname': client._altnames,
}[api_name_set]
records[tid] = norm_trade(
tid,
record,
pairs=client._AssetPairs,
pairs=pairs,
)
return records

View File

@ -43,7 +43,7 @@ from piker.accounting._mktinfo import (
# https://www.kraken.com/features/api#get-tradable-pairs
class Pair(Struct):
respname: str # idiotic bs_mktid equiv i guess?
xname: str # idiotic bs_mktid equiv i guess?
altname: str # alternate pair name
wsname: str # WebSocket pair name (if available)
aclass_base: str # asset class of base component
@ -94,7 +94,7 @@ class Pair(Struct):
make up their minds on a better key set XD
'''
return self.respname
return self.xname
@property
def price_tick(self) -> Decimal:

View File

@ -234,10 +234,13 @@ async def _reconnect_forever(
f'{url} trying (RE)CONNECT'
)
async with trio.open_nursery() as n:
cs = nobsws._cs = n.cancel_scope
ws: WebSocketConnection
async with open_websocket_url(url) as ws:
ws: WebSocketConnection
try:
async with (
trio.open_nursery() as n,
open_websocket_url(url) as ws,
):
cs = nobsws._cs = n.cancel_scope
nobsws._ws = ws
log.info(
f'{src_mod}\n'
@ -269,9 +272,11 @@ async def _reconnect_forever(
# to let tasks run **inside** the ws open block above.
nobsws._connected.set()
await trio.sleep_forever()
except HandshakeError:
log.exception(f'Retrying connection')
# ws & nursery block ends
# ws open block end
# nursery block end
nobsws._connected = trio.Event()
if cs.cancelled_caught:
log.cancel(
@ -284,7 +289,8 @@ async def _reconnect_forever(
and not nobsws._connected.is_set()
)
# -> from here, move to next reconnect attempt
# -> from here, move to next reconnect attempt iteration
# in the while loop above Bp
else:
log.exception(