Add adhoc-symbols search for ib
This gives us fast search over a known set of symbols you can't search for with the api such as futures and commodities contracts. Toss in a new client method to lookup contract details `Client.con_deats()` and avoid calling it for now from `.search_stock()` for speed; it seems originally we were doing the 2nd lookup due to weird suffixes in the `.primaryExchange` which we can just discard.pause_feeds_on_sym_switch
							parent
							
								
									3dad779c90
								
							
						
					
					
						commit
						eb5762d912
					
				|  | @ -25,7 +25,10 @@ from contextlib import asynccontextmanager | ||||||
| from dataclasses import asdict | from dataclasses import asdict | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from functools import partial | from functools import partial | ||||||
| from typing import List, Dict, Any, Tuple, Optional, AsyncIterator | from typing import ( | ||||||
|  |     List, Dict, Any, Tuple, Optional, | ||||||
|  |     AsyncIterator, Awaitable, | ||||||
|  | ) | ||||||
| import asyncio | import asyncio | ||||||
| from pprint import pformat | from pprint import pformat | ||||||
| import inspect | import inspect | ||||||
|  | @ -292,12 +295,47 @@ class Client: | ||||||
|         df = ibis.util.df(bars) |         df = ibis.util.df(bars) | ||||||
|         return bars, from_df(df) |         return bars, from_df(df) | ||||||
| 
 | 
 | ||||||
|  |     async def con_deats( | ||||||
|  |         self, | ||||||
|  |         contracts: list[Contract], | ||||||
|  | 
 | ||||||
|  |     ) -> dict[str, ContractDetails]: | ||||||
|  | 
 | ||||||
|  |         futs = [] | ||||||
|  |         for con in contracts: | ||||||
|  |             if con.primaryExchange not in _exch_skip_list: | ||||||
|  |                 futs.append(self.ib.reqContractDetailsAsync(con)) | ||||||
|  | 
 | ||||||
|  |         # batch request all details | ||||||
|  |         results = await asyncio.gather(*futs) | ||||||
|  | 
 | ||||||
|  |         # XXX: if there is more then one entry in the details list | ||||||
|  |         details = {} | ||||||
|  |         for details_set in results: | ||||||
|  | 
 | ||||||
|  |             # then the contract is so called "ambiguous". | ||||||
|  |             for d in details_set: | ||||||
|  |                 con = d.contract | ||||||
|  |                 unique_sym = f'{con.symbol}.{con.primaryExchange}' | ||||||
|  | 
 | ||||||
|  |                 as_dict = asdict(d) | ||||||
|  |                 # nested dataclass we probably don't need and that | ||||||
|  |                 # won't IPC serialize | ||||||
|  |                 as_dict.pop('secIdList') | ||||||
|  | 
 | ||||||
|  |                 details[unique_sym] = as_dict | ||||||
|  | 
 | ||||||
|  |         return details | ||||||
|  | 
 | ||||||
|     async def search_stocks( |     async def search_stocks( | ||||||
|         self, |         self, | ||||||
|         pattern: str, |         pattern: str, | ||||||
|  | 
 | ||||||
|  |         get_details: bool = False, | ||||||
|         # how many contracts to search "up to" |         # how many contracts to search "up to" | ||||||
|         upto: int = 3, |         upto: int = 3, | ||||||
|     ) -> Dict[str, ContractDetails]: | 
 | ||||||
|  |     ) -> dict[str, ContractDetails]: | ||||||
|         """Search for stocks matching provided ``str`` pattern. |         """Search for stocks matching provided ``str`` pattern. | ||||||
| 
 | 
 | ||||||
|         Return a dictionary of ``upto`` entries worth of contract details. |         Return a dictionary of ``upto`` entries worth of contract details. | ||||||
|  | @ -305,37 +343,22 @@ class Client: | ||||||
|         descriptions = await self.ib.reqMatchingSymbolsAsync(pattern) |         descriptions = await self.ib.reqMatchingSymbolsAsync(pattern) | ||||||
| 
 | 
 | ||||||
|         if descriptions is not None: |         if descriptions is not None: | ||||||
|  |             descrs = descriptions[:upto] | ||||||
| 
 | 
 | ||||||
|             futs = [] |             if get_details: | ||||||
|             for d in descriptions: |                 return await self.con_deats([d.contract for d in descrs]) | ||||||
|                 con = d.contract |  | ||||||
|                 if con.primaryExchange not in _exch_skip_list: |  | ||||||
|                     futs.append(self.ib.reqContractDetailsAsync(con)) |  | ||||||
| 
 | 
 | ||||||
|             # batch request all details |             else: | ||||||
|             results = await asyncio.gather(*futs) |                 results = {} | ||||||
| 
 |                 for d in descrs: | ||||||
|             # XXX: if there is more then one entry in the details list |  | ||||||
|             details = {} |  | ||||||
|             for details_set in results: |  | ||||||
| 
 |  | ||||||
|                 # then the contract is so called "ambiguous". |  | ||||||
|                 for d in details_set: |  | ||||||
|                     con = d.contract |                     con = d.contract | ||||||
|                     unique_sym = f'{con.symbol}.{con.primaryExchange}' |                     # sometimes there's a weird extra suffix returned | ||||||
| 
 |                     # from search? | ||||||
|                     as_dict = asdict(d) |                     exch = con.primaryExchange.rsplit('.')[0] | ||||||
|                     # nested dataclass we probably don't need and that |                     unique_sym = f'{con.symbol}.{exch}' | ||||||
|                     # won't IPC serialize |                     results[unique_sym] = {} | ||||||
|                     as_dict.pop('secIdList') |  | ||||||
| 
 |  | ||||||
|                     details[unique_sym] = as_dict |  | ||||||
| 
 |  | ||||||
|                     if len(details) == upto: |  | ||||||
|                         return details |  | ||||||
| 
 |  | ||||||
|             return details |  | ||||||
| 
 | 
 | ||||||
|  |                 return results | ||||||
|         else: |         else: | ||||||
|             return {} |             return {} | ||||||
| 
 | 
 | ||||||
|  | @ -345,20 +368,12 @@ class Client: | ||||||
|         # how many contracts to search "up to" |         # how many contracts to search "up to" | ||||||
|         upto: int = 3, |         upto: int = 3, | ||||||
|         asdicts: bool = True, |         asdicts: bool = True, | ||||||
|  | 
 | ||||||
|     ) -> Dict[str, ContractDetails]: |     ) -> Dict[str, ContractDetails]: | ||||||
| 
 | 
 | ||||||
|         # TODO add search though our adhoc-locally defined symbol set |         # TODO add search though our adhoc-locally defined symbol set | ||||||
|         # for futes/cmdtys/ |         # for futes/cmdtys/ | ||||||
|         return await self.search_stocks(pattern, upto, asdicts) |         return await self.search_stocks(pattern, upto, get_details=True) | ||||||
| 
 |  | ||||||
|     async def search_futes( |  | ||||||
|         self, |  | ||||||
|         pattern: str, |  | ||||||
|         # how many contracts to search "up to" |  | ||||||
|         upto: int = 3, |  | ||||||
|         asdicts: bool = True, |  | ||||||
|     ) -> Dict[str, ContractDetails]: |  | ||||||
|         raise NotImplementedError |  | ||||||
| 
 | 
 | ||||||
|     async def get_cont_fute( |     async def get_cont_fute( | ||||||
|         self, |         self, | ||||||
|  | @ -1564,20 +1579,51 @@ async def open_symbol_search( | ||||||
|             log.debug(f'searching for {pattern}') |             log.debug(f'searching for {pattern}') | ||||||
| 
 | 
 | ||||||
|             last = time.time() |             last = time.time() | ||||||
|             results = await _trio_run_client_method( |  | ||||||
|                 method='search_stocks', |  | ||||||
|                 pattern=pattern, |  | ||||||
|                 upto=5, |  | ||||||
|             ) |  | ||||||
|             log.debug(f'got results {results.keys()}') |  | ||||||
| 
 | 
 | ||||||
|             log.debug("fuzzy matching") |             # async batch search using api stocks endpoint and module | ||||||
|             matches = fuzzy.extractBests( |             # defined adhoc symbol set. | ||||||
|  |             stock_results = [] | ||||||
|  | 
 | ||||||
|  |             async def stash_results(target: Awaitable[list]): | ||||||
|  |                 stock_results.extend(await target) | ||||||
|  | 
 | ||||||
|  |             async with trio.open_nursery() as sn: | ||||||
|  |                 sn.start_soon( | ||||||
|  |                     stash_results, | ||||||
|  |                     _trio_run_client_method( | ||||||
|  |                         method='search_stocks', | ||||||
|  |                         pattern=pattern, | ||||||
|  |                         upto=5, | ||||||
|  |                     ) | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
|  |                 # trigger async request | ||||||
|  |                 await trio.sleep(0) | ||||||
|  | 
 | ||||||
|  |                 # match against our ad-hoc set immediately | ||||||
|  |                 adhoc_matches = fuzzy.extractBests( | ||||||
|  |                     pattern, | ||||||
|  |                     list(_adhoc_futes_set), | ||||||
|  |                     score_cutoff=90, | ||||||
|  |                 ) | ||||||
|  |                 log.info(f'fuzzy matched adhocs: {adhoc_matches}') | ||||||
|  |                 adhoc_match_results = {} | ||||||
|  |                 if adhoc_matches: | ||||||
|  |                     # TODO: do we need to pull contract details? | ||||||
|  |                     adhoc_match_results = {i[0]: {} for i in adhoc_matches} | ||||||
|  | 
 | ||||||
|  |             log.debug(f'fuzzy matching stocks {stock_results}') | ||||||
|  |             stock_matches = fuzzy.extractBests( | ||||||
|                 pattern, |                 pattern, | ||||||
|                 results, |                 stock_results, | ||||||
|                 score_cutoff=50, |                 score_cutoff=50, | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|             matches = {item[2]: item[0] for item in matches} |             matches = adhoc_match_results | { | ||||||
|  |                 item[0]: {} for item in stock_matches | ||||||
|  |             } | ||||||
|  |             # TODO: we used to deliver contract details | ||||||
|  |             # {item[2]: item[0] for item in stock_matches} | ||||||
|  | 
 | ||||||
|             log.debug(f"sending matches: {matches.keys()}") |             log.debug(f"sending matches: {matches.keys()}") | ||||||
|             await stream.send(matches) |             await stream.send(matches) | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue