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