diff --git a/piker/brokers/ib/api.py b/piker/brokers/ib/api.py index 6d214d22..fa9a7b72 100644 --- a/piker/brokers/ib/api.py +++ b/piker/brokers/ib/api.py @@ -424,23 +424,15 @@ class Client: # one set per future result details = {} for details_set in results: + # XXX: if there is more then one entry in the details list # then the contract is so called "ambiguous". for d in details_set: - con = d.contract - key = '.'.join([ - con.symbol, - con.primaryExchange or con.exchange, - ]) - expiry = con.lastTradeDateOrContractMonth - if expiry: - key += f'.{expiry}' - - # nested dataclass we probably don't need and that - # won't IPC serialize.. + # nested dataclass we probably don't need and that won't + # IPC serialize.. d.secIdList = '' - + key, calc_price = con2fqsn(d.contract) details[key] = d return details @@ -470,7 +462,7 @@ class Client: self, pattern: str, # how many contracts to search "up to" - upto: int = 3, + upto: int = 6, asdicts: bool = True, ) -> dict[str, ContractDetails]: @@ -522,10 +514,14 @@ class Client: elif sectype == 'CASH': dst, src = tract.localSymbol.split('.') pair_key = "/".join([dst, src]) - tract.exchange = 'FOREX' - results[f'{pair_key}.forex'] = tract + exch = tract.exchange.lower() + results[f'{pair_key}.{exch}'] = tract results.pop(key) + # XXX: again seems to trigger the weird tractor + # bug with the debugger.. + # assert 0 + return results async def get_fute( @@ -595,12 +591,11 @@ class Client: # another hack for forex pairs lul. if ( - '.forex' in symbol - # and not '.ib' in pattern + '.idealpro' in symbol # or '/' in symbol ): - exch = 'FOREX' - symbol = symbol.removesuffix('.forex') + exch = 'IDEALPRO' + symbol = symbol.removesuffix('.idealpro') if '/' in symbol: symbol, currency = symbol.split('/') @@ -665,7 +660,7 @@ class Client: ) elif ( - exch in ('FOREX') + exch in ('IDEALPRO') or sectype == 'CASH' ): # if '/' in symbol: @@ -948,6 +943,58 @@ class Client: return self.ib.positions(account=account) +def con2fqsn( + con: Contract, + _cache: dict[int, (str, bool)] = {} + +) -> tuple[str, bool]: + ''' + Convert contracts to fqsn-style strings to be used both in symbol-search + matching and as feed tokens passed to the front end data deed layer. + + Previously seen contracts are cached by id. + + ''' + # should be real volume for this contract by default + calc_price = False + if con.conId: + try: + return _cache[con.conId] + except KeyError: + pass + + suffix = con.primaryExchange or con.exchange + symbol = con.symbol + expiry = con.lastTradeDateOrContractMonth or '' + + match con: + case ibis.Commodity(): + # commodities and forex don't have an exchange name and + # no real volume so we have to calculate the price + suffix = con.secType + + # no real volume on this tract + calc_price = True + + case ibis.Forex() | ibis.Contract(secType='CASH'): + dst, src = con.localSymbol.split('.') + symbol = ''.join([dst, src]) + suffix = con.exchange + + # no real volume on forex feeds.. + calc_price = True + + # append a `.` to the returned symbol + # key for derivatives that normally is the expiry + # date key. + if expiry: + suffix += f'.{expiry}' + + fqsn_key = '.'.join((symbol, suffix)).lower() + _cache[con.conId] = fqsn_key, calc_price + return fqsn_key, calc_price + + # per-actor API ep caching _client_cache: dict[tuple[str, int], Client] = {} _scan_ignore: set[tuple[str, int]] = set() diff --git a/piker/brokers/ib/feed.py b/piker/brokers/ib/feed.py index 0619011a..c3ac985f 100644 --- a/piker/brokers/ib/feed.py +++ b/piker/brokers/ib/feed.py @@ -42,6 +42,7 @@ from piker.data._sharedmem import ShmArray from .._util import SymbolNotFound, NoData from .api import ( # _adhoc_futes_set, + con2fqsn, log, load_aio_clients, ibis, @@ -559,47 +560,18 @@ async def open_aio_quote_stream( # TODO: cython/mypyc/numba this! +# or we can at least cache a majority of the values +# except for the ones we expect to change?.. def normalize( ticker: Ticker, calc_price: bool = False ) -> dict: - # should be real volume for this contract by default - calc_price = False - # check for special contract types con = ticker.contract - symbol = con.symbol - if type(con) in ( - ibis.Commodity, - ): - # commodities and forex don't have an exchange name and - # no real volume so we have to calculate the price - suffix = con.secType - # no real volume on this tract - calc_price = True - - elif type(con) in ( - ibis.Forex, - ): - suffix = 'forex' - symbol = con.pair() - # no real volume on forex feeds.. - calc_price = True - - else: - suffix = con.primaryExchange - if not suffix: - suffix = con.exchange - - # append a `.` to the returned symbol - # key for derivatives that normally is the expiry - # date key. - expiry = con.lastTradeDateOrContractMonth - if expiry: - suffix += f'.{expiry}' + fqsn, calc_price = con2fqsn(con) # convert named tuples to dicts so we send usable keys new_ticks = [] @@ -631,9 +603,7 @@ def normalize( # generate fqsn with possible specialized suffix # for derivatives, note the lowercase. - data['symbol'] = data['fqsn'] = '.'.join( - (symbol, suffix) - ).lower() + data['symbol'] = data['fqsn'] = fqsn # convert named tuples to dicts for transport tbts = data.get('tickByTicks')