From 1866dd181294b9c6cda3d3873bacb882df5a1c9d Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sat, 29 Dec 2018 15:44:32 -0500 Subject: [PATCH] Fix for adjusted contracts subscription bug If quotes are pushed using the adjusted contract symbol (i.e. with trailing '-1' suffix) the subscriber won't receive them under the normal symbol. The logic was wrong for determining whether to add a suffix (was failing for any symbol with an exchange suffix) which was causing normal data feed subscriptions to fail to match in every case. I did some testing of the `optionsIds` parameter to the option quote endpoint and found that it limits you to 100 symbols so it's not practical for real-time "all-strike"" chain updating; we have to stick to filters for now. The only real downside of this is that it seems multiple filters across multiple symbols is quite latent. I need to toy with it more to be sure it's not something slow on the client side. Oh, and store option contract to ids in a `dict` for now as we may want to try the `optionsIds` thing again down the road as I coordinate with the QT tech team. --- piker/brokers/questrade.py | 66 +++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/piker/brokers/questrade.py b/piker/brokers/questrade.py index fca579a4..2a588562 100644 --- a/piker/brokers/questrade.py +++ b/piker/brokers/questrade.py @@ -107,6 +107,8 @@ class _API: ] resp = await self._sess.post( path=f'/markets/quotes/options', + # XXX: b'{"code":1024,"message":"The size of the array requested is not valid: optionIds"}' + # ^ what I get when trying to use too many ids manually... json={'filters': filters, 'optionIds': option_ids} ) return resproc(resp, log)['optionQuotes'] @@ -125,8 +127,8 @@ class Client: self.access_data = {} self._reload_config(config) self._symbol_cache: Dict[str, int] = {} - self._contracts2expiries = {} self._optids2contractinfo = {} + self._contract2ids = {} def _reload_config(self, config=None, **kwargs): log.warn("Reloading access config data") @@ -317,14 +319,13 @@ class Client: ): for chain in byroot['chainPerRoot']: optroot = chain['optionRoot'] - suffix = '' # handle QTs "adjusted contracts" (aka adjusted for # the underlying in some way; usually has a '(1)' in # the expiry key in their UI) - adjusted_contracts = optroot != key.symbol - if adjusted_contracts: - suffix = '(' + optroot[len(key.symbol):] + ')' + adjusted_contracts = optroot not in key.symbol + tail = optroot[len(key.symbol):] + suffix = '-' + tail if adjusted_contracts else '' by_key[ ContractsKey( @@ -344,12 +345,16 @@ class Client: for key, contract_type in ( ('callSymbolId', 'call'), ('putSymbolId', 'put') ): - self._optids2contractinfo[ - ids[key]] = { - 'strike': strike, - 'expiry': tup.expiry, - 'contract_type': contract_type, - } + contract_int_id = ids[key] + self._optids2contractinfo[contract_int_id] = { + 'strike': strike, + 'expiry': tup.expiry, + 'contract_type': contract_type, + 'contract_key': tup, + } + # store ids per contract + self._contract2ids.setdefault( + tup, set()).add(contract_int_id) return by_key async def option_chains( @@ -359,22 +364,31 @@ class Client: ) -> Dict[str, Dict[str, Dict[str, Any]]]: """Return option chain snap quote for each ticker in ``symbols``. """ - batch = [] - for key, bystrike in contracts.items(): - quotes = await self.api.option_quotes({key: bystrike}) - for quote in quotes: - # index by .symbol, .expiry since that's what - # a subscriber (currently) sends initially - quote['key'] = (key[0], key[2]) - # update with expiry and strike (Obviously the - # QT api designers are using some kind of severely - # stupid disparate table system where they keep - # contract info in a separate table from the quote format - # keys. I'm really not surprised though - windows shop..) - quote.update(self._optids2contractinfo[quote['symbolId']]) - batch.extend(quotes) + quotes = await self.api.option_quotes(contracts=contracts) + # XXX the below doesn't work so well due to the symbol count + # limit per quote request + # quotes = await self.api.option_quotes(option_ids=list(contract_ids)) + for quote in quotes: + id = quote['symbolId'] + contract_info = self._optids2contractinfo[id].copy() + key = contract_info.pop('contract_key') - return batch + # XXX TODO: this currently doesn't handle adjusted contracts + # (i.e. ones that we stick a '(1)' after) + + # index by .symbol, .expiry since that's what + # a subscriber (currently) sends initially + quote['key'] = (key.symbol, key.expiry) + + # update with expiry and strike (Obviously the + # QT api designers are using some kind of severely + # stupid disparate table system where they keep + # contract info in a separate table from the quote format + # keys. I'm really not surprised though - windows shop..) + # quote.update(self._optids2contractinfo[quote['symbolId']]) + quote.update(contract_info) + + return quotes async def token_refresher(client):