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
Tyler Goodlet 2021-09-02 10:46:20 -04:00
parent 3dad779c90
commit eb5762d912
1 changed files with 95 additions and 49 deletions

View File

@ -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,23 +295,14 @@ class Client:
df = ibis.util.df(bars) df = ibis.util.df(bars)
return bars, from_df(df) return bars, from_df(df)
async def search_stocks( async def con_deats(
self, self,
pattern: str, contracts: list[Contract],
# how many contracts to search "up to"
upto: int = 3,
) -> Dict[str, ContractDetails]:
"""Search for stocks matching provided ``str`` pattern.
Return a dictionary of ``upto`` entries worth of contract details. ) -> dict[str, ContractDetails]:
"""
descriptions = await self.ib.reqMatchingSymbolsAsync(pattern)
if descriptions is not None:
futs = [] futs = []
for d in descriptions: for con in contracts:
con = d.contract
if con.primaryExchange not in _exch_skip_list: if con.primaryExchange not in _exch_skip_list:
futs.append(self.ib.reqContractDetailsAsync(con)) futs.append(self.ib.reqContractDetailsAsync(con))
@ -331,11 +325,40 @@ class Client:
details[unique_sym] = as_dict details[unique_sym] = as_dict
if len(details) == upto:
return details return details
return details async def search_stocks(
self,
pattern: str,
get_details: bool = False,
# how many contracts to search "up to"
upto: int = 3,
) -> dict[str, ContractDetails]:
"""Search for stocks matching provided ``str`` pattern.
Return a dictionary of ``upto`` entries worth of contract details.
"""
descriptions = await self.ib.reqMatchingSymbolsAsync(pattern)
if descriptions is not None:
descrs = descriptions[:upto]
if get_details:
return await self.con_deats([d.contract for d in descrs])
else:
results = {}
for d in descrs:
con = d.contract
# sometimes there's a weird extra suffix returned
# from search?
exch = con.primaryExchange.rsplit('.')[0]
unique_sym = f'{con.symbol}.{exch}'
results[unique_sym] = {}
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(
# async batch search using api stocks endpoint and module
# 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', method='search_stocks',
pattern=pattern, pattern=pattern,
upto=5, upto=5,
) )
log.debug(f'got results {results.keys()}') )
log.debug("fuzzy matching") # trigger async request
matches = fuzzy.extractBests( await trio.sleep(0)
# match against our ad-hoc set immediately
adhoc_matches = fuzzy.extractBests(
pattern, pattern,
results, 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,
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)