Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backward compat support for Transaction.sym: Symbol #477

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 53 additions & 10 deletions piker/brokers/ib/broker.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,10 @@
BrokerdFill,
BrokerdError,
)
from piker.data._source import Symbol
from piker.data._source import (
Symbol,
float_digits,
)
from .api import (
_accounts2clients,
con2fqsn,
Expand Down Expand Up @@ -304,6 +307,9 @@ async def update_ledger_from_api_trades(

entry['listingExchange'] = pexch

# pack in the ``Contract.secType``
entry['asset_type'] = condict['secType']

conf = get_config()
entries = api_trades_to_ledger_entries(
conf['accounts'].inverse,
Expand Down Expand Up @@ -616,9 +622,10 @@ async def trades_dialogue(
# from the api trades it seems we get a key
# error from ``update[bsuid]`` ?
pp = table.pps[bsuid]
pairinfo = pp.symbol
if msg.size != pp.size:
log.error(
f'Position mismatch {pp.symbol.front_fqsn()}:\n'
f'Pos size mismatch {pairinfo.front_fqsn()}:\n'
f'ib: {msg.size}\n'
f'piker: {pp.size}\n'
)
Expand Down Expand Up @@ -1095,13 +1102,15 @@ def norm_trade_records(

'''
records: list[Transaction] = []
for tid, record in ledger.items():

for tid, record in ledger.items():
conid = record.get('conId') or record['conid']
comms = record.get('commission')
if comms is None:
comms = -1*record['ibCommission']

price = record.get('price') or record['tradePrice']
price_tick_digits = float_digits(price)

# the api doesn't do the -/+ on the quantity for you but flex
# records do.. are you fucking serious ib...!?
Expand Down Expand Up @@ -1144,9 +1153,14 @@ def norm_trade_records(

# special handling of symbol extraction from
# flex records using some ad-hoc schema parsing.
instr = record.get('assetCategory')
if instr == 'FUT':
symbol = record['description'][:3]
asset_type: str = record.get('assetCategory') or record['secType']

# TODO: XXX: WOA this is kinda hacky.. probably
# should figure out the correct future pair key more
# explicitly and consistently?
if asset_type == 'FUT':
# (flex) ledger entries don't have any simple 3-char key?
symbol = record['symbol'][:3]

# try to build out piker fqsn from record.
expiry = record.get(
Expand All @@ -1156,10 +1170,34 @@ def norm_trade_records(
suffix = f'{exch}.{expiry}'
expiry = pendulum.parse(expiry)

fqsn = Symbol.from_fqsn(
src: str = record['currency']

pair = Symbol.from_fqsn(
fqsn=f'{symbol}.{suffix}.ib',
info={},
).front_fqsn().rstrip('.ib')
info={
'tick_size_digits': price_tick_digits,

# NOTE: for "legacy" assets, volume is normally discreet, not
# a float, but we keep a digit in case the suitz decide
# to get crazy and change it; we'll be kinda ready
# schema-wise..
'lot_size_digits': 1,

# TODO: remove when we switching from
# ``Symbol`` -> ``MktPair``
'asset_type': asset_type,

# TODO: figure out a target fin-type name
# set and normalize to that here!
'dst_type': asset_type.lower(),

# starting to use new key naming as in ``MktPair``
# type have drafted...
'src': src,
'src_type': 'fiat',
},
)
fqsn = pair.front_fqsn().rstrip('.ib')

# NOTE: for flex records the normal fields for defining an fqsn
# sometimes won't be available so we rely on two approaches for
Expand All @@ -1175,6 +1213,7 @@ def norm_trade_records(
records,
Transaction(
fqsn=fqsn,
sym=pair,
tid=tid,
size=size,
price=price,
Expand All @@ -1201,7 +1240,11 @@ def parse_flex_dt(

def api_trades_to_ledger_entries(
accounts: bidict,
trade_entries: list[object],

# TODO: maybe we should just be passing through the
# ``ib_insync.order.Trade`` instance directly here
# instead of pre-casting to dicts?
trade_entries: list[dict],

) -> dict:
'''
Expand Down
2 changes: 1 addition & 1 deletion piker/brokers/ib/feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -770,7 +770,7 @@ def mk_init_msgs() -> dict[str, dict]:

syminfo['price_tick_size'] = max(syminfo['minTick'], min_tick)

# for "traditional" assets, volume is normally discreet, not
# for "legacy" assets, volume is normally discreet, not
# a float
syminfo['lot_tick_size'] = 0.0

Expand Down
131 changes: 106 additions & 25 deletions piker/brokers/kraken/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
import trio

from piker import config
from piker.data.types import Struct
from piker.data._source import Symbol
from piker.brokers._util import (
resproc,
SymbolNotFound,
Expand Down Expand Up @@ -113,11 +115,53 @@ class InvalidKey(ValueError):
'''


# https://www.kraken.com/features/api#get-tradable-pairs
class Pair(Struct):
altname: str # alternate pair name
wsname: str # WebSocket pair name (if available)
aclass_base: str # asset class of base component
base: str # asset id of base component
aclass_quote: str # asset class of quote component
quote: str # asset id of quote component
lot: str # volume lot size

cost_decimals: int
costmin: float
pair_decimals: int # scaling decimal places for pair
lot_decimals: int # scaling decimal places for volume

# amount to multiply lot volume by to get currency volume
lot_multiplier: float

# array of leverage amounts available when buying
leverage_buy: list[int]
# array of leverage amounts available when selling
leverage_sell: list[int]

# fee schedule array in [volume, percent fee] tuples
fees: list[tuple[int, float]]

# maker fee schedule array in [volume, percent fee] tuples (if on
# maker/taker)
fees_maker: list[tuple[int, float]]

fee_volume_currency: str # volume discount currency
margin_call: str # margin call level
margin_stop: str # stop-out/liquidation margin level
ordermin: float # minimum order volume for pair
tick_size: float # min price step size
status: str

short_position_limit: float = 0
long_position_limit: float = float('inf')


class Client:

# global symbol normalization table
_ntable: dict[str, str] = {}
_atable: bidict[str, str] = bidict()
_pairs: dict[str, Pair] = {}

def __init__(
self,
Expand All @@ -133,13 +177,12 @@ def __init__(
'krakenex/2.1.0 (+https://github.com/veox/python3-krakenex)'
})
self.conf: dict[str, str] = config
self._pairs: list[str] = []
self._name = name
self._api_key = api_key
self._secret = secret

@property
def pairs(self) -> dict[str, Any]:
def pairs(self) -> dict[str, Pair]:
if self._pairs is None:
raise RuntimeError(
"Make sure to run `cache_symbols()` on startup!"
Expand Down Expand Up @@ -295,15 +338,28 @@ async def get_xfers(

trans: dict[str, Transaction] = {}
for entry in xfers:
# look up the normalized name
asset = self._atable[entry['asset']].lower()

# look up the normalized name and asset info
asset_key = entry['asset']
asset_info = self.assets[asset_key]
asset = self._atable[asset_key].lower()

# XXX: this is in the asset units (likely) so it isn't
# quite the same as a commisions cost necessarily..)
cost = float(entry['fee'])

fqsn = asset + '.kraken'
pairinfo = Symbol.from_fqsn(
fqsn,
info={
'asset_type': 'crypto',
'lot_tick_size': asset_info['decimals'],
},
)

tran = Transaction(
fqsn=asset + '.kraken',
fqsn=fqsn,
sym=pairinfo,
tid=entry['txid'],
dt=pendulum.from_timestamp(entry['time']),
bsuid=f'{asset}{src_asset}',
Expand All @@ -317,7 +373,7 @@ async def get_xfers(
price='NaN',

# XXX: see note above
cost=0,
cost=cost,
)
trans[tran.tid] = tran

Expand Down Expand Up @@ -372,7 +428,7 @@ async def symbol_info(
self,
pair: Optional[str] = None,

) -> dict[str, dict[str, str]]:
) -> dict[str, Pair] | Pair:

if pair is not None:
pairs = {'pair': pair}
Expand All @@ -389,19 +445,36 @@ async def symbol_info(

if pair is not None:
_, data = next(iter(pairs.items()))
return data
return Pair(**data)
else:
return pairs
return {key: Pair(**data) for key, data in pairs.items()}

async def cache_symbols(
self,
) -> dict:
async def cache_symbols(self) -> dict:
'''
Load all market pair info build and cache it for downstream use.

A ``._ntable: dict[str, str]`` is available for mapping the
websocket pair name-keys and their http endpoint API (smh)
equivalents to the "alternative name" which is generally the one
we actually want to use XD

'''
if not self._pairs:
self._pairs = await self.symbol_info()
self._pairs.update(await self.symbol_info())

# table of all ws and rest keys to their alt-name values.
ntable: dict[str, str] = {}

for rest_key in list(self._pairs.keys()):

pair: Pair = self._pairs[rest_key]
altname = pair.altname
wsname = pair.wsname
ntable[rest_key] = ntable[wsname] = altname

ntable = {}
for restapikey, info in self._pairs.items():
ntable[restapikey] = ntable[info['wsname']] = info['altname']
# register the pair under all monikers, a giant flat
# surjection of all possible names to each info obj.
self._pairs[altname] = self._pairs[wsname] = pair

self._ntable.update(ntable)

Expand All @@ -411,26 +484,34 @@ async def search_symbols(
self,
pattern: str,
limit: int = None,

) -> dict[str, Any]:
if self._pairs is not None:
data = self._pairs
else:
data = await self.symbol_info()
'''
Search for a symbol by "alt name"..

It is expected that the ``Client._pairs`` table
gets populated before conducting the underlying fuzzy-search
over the pair-key set.

'''
if not len(self._pairs):
await self.cache_symbols()
assert self._pairs, '`Client.cache_symbols()` was never called!?'

matches = fuzzy.extractBests(
pattern,
data,
self._pairs,
score_cutoff=50,
)
# repack in dict form
return {item[0]['altname']: item[0] for item in matches}
return {item[0].altname: item[0] for item in matches}

async def bars(
self,
symbol: str = 'XBTUSD',

# UTC 2017-07-02 12:53:20
since: Optional[Union[int, datetime]] = None,
since: Union[int, datetime] | None = None,
count: int = 720, # <- max allowed per query
as_np: bool = True,

Expand Down Expand Up @@ -506,15 +587,15 @@ async def bars(
def normalize_symbol(
cls,
ticker: str
) -> str:
) -> tuple[str, Pair]:
'''
Normalize symbol names to to a 3x3 pair from the global
definition map which we build out from the data retreived from
the 'AssetPairs' endpoint, see methods above.

'''
ticker = cls._ntable[ticker]
return ticker.lower()
return ticker.lower(), cls._pairs[ticker]


@acm
Expand Down
Loading