Unbreak regular searches and stock lookups..
Change `.find_contract()` -> `.find_contracts()` to allow multi-search for so called "ambiguous" contracts (like for `Future`s) such that the method now returns a `list` of tracts and populates the contract cache with all specific tracts retrieved. Let it take in an (unvalidated) contract that will be fqsn-style-tokenized such that it can be called from `.search_symbols()` (though we're not quite yet XD). More stuff, - add `Client.parse_patt2fqsn()` which is an fqsn to token unpacker built from the original logic in the old `.find_contract()`. - handle fiat/forex pairs with the `'CASH'` sectype. - add a flag to allow unqualified contracts to fail with a warning msg. - populate the client's contract cache with all expiries of an ambiguous derivative. - allow `.con_deats()` to warn msg instead of raise on def-not-found. - add commented `assert 0` which was triggering a debugger deadlock in `tractor` which we still haven't been able to create a unit test for.fix_forex
							parent
							
								
									0580b204a3
								
							
						
					
					
						commit
						0ef5da0881
					
				|  | @ -29,6 +29,7 @@ import itertools | ||||||
| from math import isnan | from math import isnan | ||||||
| from typing import ( | from typing import ( | ||||||
|     Any, |     Any, | ||||||
|  |     Optional, | ||||||
|     Union, |     Union, | ||||||
| ) | ) | ||||||
| import asyncio | import asyncio | ||||||
|  | @ -43,8 +44,10 @@ import trio | ||||||
| import tractor | import tractor | ||||||
| from tractor import to_asyncio | from tractor import to_asyncio | ||||||
| import ib_insync as ibis | import ib_insync as ibis | ||||||
| from ib_insync.wrapper import RequestError | from ib_insync.contract import ( | ||||||
| from ib_insync.contract import Contract, ContractDetails |     Contract, | ||||||
|  |     ContractDetails, | ||||||
|  | ) | ||||||
| from ib_insync.order import Order | from ib_insync.order import Order | ||||||
| from ib_insync.ticker import Ticker | from ib_insync.ticker import Ticker | ||||||
| from ib_insync.objects import ( | from ib_insync.objects import ( | ||||||
|  | @ -53,7 +56,10 @@ from ib_insync.objects import ( | ||||||
|     Execution, |     Execution, | ||||||
|     CommissionReport, |     CommissionReport, | ||||||
| ) | ) | ||||||
| from ib_insync.wrapper import Wrapper | from ib_insync.wrapper import ( | ||||||
|  |     Wrapper, | ||||||
|  |     RequestError, | ||||||
|  | ) | ||||||
| from ib_insync.client import Client as ib_Client | from ib_insync.client import Client as ib_Client | ||||||
| import numpy as np | import numpy as np | ||||||
| 
 | 
 | ||||||
|  | @ -184,12 +190,12 @@ _adhoc_futes_set = { | ||||||
|     'ethusdrr.cmecrypto', |     'ethusdrr.cmecrypto', | ||||||
| 
 | 
 | ||||||
|     # agriculture |     # agriculture | ||||||
|     'he.globex',  # lean hogs |     'he.nymex',  # lean hogs | ||||||
|     'le.globex',  # live cattle (geezers) |     'le.nymex',  # live cattle (geezers) | ||||||
|     'gf.globex',  # feeder cattle (younguns) |     'gf.nymex',  # feeder cattle (younguns) | ||||||
| 
 | 
 | ||||||
|     # raw |     # raw | ||||||
|     'lb.globex',  # random len lumber |     'lb.nymex',  # random len lumber | ||||||
| 
 | 
 | ||||||
|     # metals |     # metals | ||||||
|     'xauusd.cmdty',  # gold spot |     'xauusd.cmdty',  # gold spot | ||||||
|  | @ -247,6 +253,7 @@ _exch_skip_list = { | ||||||
|     'VALUE', |     'VALUE', | ||||||
|     'FUNDSERV', |     'FUNDSERV', | ||||||
|     'SWB2', |     'SWB2', | ||||||
|  |     'PSE', | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| # https://misc.interactivebrokers.com/cstools/contract_info/v3.10/index.php?action=Conid%20Info&wlId=IB&conid=69067924 | # https://misc.interactivebrokers.com/cstools/contract_info/v3.10/index.php?action=Conid%20Info&wlId=IB&conid=69067924 | ||||||
|  | @ -349,7 +356,7 @@ class Client: | ||||||
| 
 | 
 | ||||||
|         _enters += 1 |         _enters += 1 | ||||||
| 
 | 
 | ||||||
|         contract = await self.find_contract(fqsn) |         contract = (await self.find_contracts(fqsn))[0] | ||||||
|         bars_kwargs.update(getattr(contract, 'bars_kwargs', {})) |         bars_kwargs.update(getattr(contract, 'bars_kwargs', {})) | ||||||
| 
 | 
 | ||||||
|         # _min = min(2000*100, count) |         # _min = min(2000*100, count) | ||||||
|  | @ -404,12 +411,19 @@ class Client: | ||||||
|                 futs.append(self.ib.reqContractDetailsAsync(con)) |                 futs.append(self.ib.reqContractDetailsAsync(con)) | ||||||
| 
 | 
 | ||||||
|         # batch request all details |         # batch request all details | ||||||
|         results = await asyncio.gather(*futs) |         try: | ||||||
|  |             results = await asyncio.gather(*futs) | ||||||
|  |         except RequestError as err: | ||||||
|  |             msg = err.message | ||||||
|  |             if ( | ||||||
|  |                 'No security definition' in msg | ||||||
|  |             ): | ||||||
|  |                 log.warning(f'{msg}: {contracts}') | ||||||
|  |                 return {} | ||||||
| 
 | 
 | ||||||
|         # one set per future result |         # one set per future result | ||||||
|         details = {} |         details = {} | ||||||
|         for details_set in results: |         for details_set in results: | ||||||
| 
 |  | ||||||
|             # XXX: if there is more then one entry in the details list |             # XXX: if there is more then one entry in the details list | ||||||
|             # then the contract is so called "ambiguous". |             # then the contract is so called "ambiguous". | ||||||
|             for d in details_set: |             for d in details_set: | ||||||
|  | @ -477,21 +491,32 @@ class Client: | ||||||
|             if sectype == 'IND': |             if sectype == 'IND': | ||||||
|                 results[f'{sym}.IND'] = tract |                 results[f'{sym}.IND'] = tract | ||||||
|                 results.pop(key) |                 results.pop(key) | ||||||
|                 exch = tract.exchange |                 # exch = tract.exchange | ||||||
| 
 | 
 | ||||||
|                 if exch in _futes_venues: |                 # XXX: add back one of these to get the weird deadlock | ||||||
|  |                 # on the debugger from root without the latest | ||||||
|  |                 # maybe_wait_for_debugger() fix in the `open_context()` | ||||||
|  |                 # exit. | ||||||
|  |                 # assert 0 | ||||||
|  |                 # if con.exchange not in _exch_skip_list: | ||||||
|  | 
 | ||||||
|  |                 exch = tract.exchange | ||||||
|  |                 if exch not in _exch_skip_list: | ||||||
|                     # try get all possible contracts for symbol as per, |                     # try get all possible contracts for symbol as per, | ||||||
|                     # https://interactivebrokers.github.io/tws-api/basic_contracts.html#fut |                     # https://interactivebrokers.github.io/tws-api/basic_contracts.html#fut | ||||||
|                     con = ibis.Future( |                     con = ibis.Future( | ||||||
|                         symbol=sym, |                         symbol=sym, | ||||||
|                         exchange=exch, |                         exchange=exch, | ||||||
|                     ) |                     ) | ||||||
|                     try: |                     # TODO: make this work, think it's something to do | ||||||
|                         all_deats = await self.con_deats([con]) |                     # with the qualify flag. | ||||||
|                         results |= all_deats |                     # cons = await self.find_contracts( | ||||||
| 
 |                     #     contract=con, | ||||||
|                     except RequestError as err: |                     #     err_on_qualify=False, | ||||||
|                         log.warning(err.message) |                     # ) | ||||||
|  |                     # if cons: | ||||||
|  |                     all_deats = await self.con_deats([con]) | ||||||
|  |                     results |= all_deats | ||||||
| 
 | 
 | ||||||
|             # forex pairs |             # forex pairs | ||||||
|             elif sectype == 'CASH': |             elif sectype == 'CASH': | ||||||
|  | @ -539,13 +564,11 @@ class Client: | ||||||
|             ibis.Contract(conId=conid) |             ibis.Contract(conId=conid) | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     async def find_contract( |     def parse_patt2fqsn( | ||||||
|         self, |         self, | ||||||
|         pattern: str, |         pattern: str, | ||||||
|         currency: str = '', |  | ||||||
|         **kwargs, |  | ||||||
| 
 | 
 | ||||||
|     ) -> Contract: |     ) -> tuple[str, str, str, str]: | ||||||
| 
 | 
 | ||||||
|         # TODO: we can't use this currently because |         # TODO: we can't use this currently because | ||||||
|         # ``wrapper.starTicker()`` currently cashes ticker instances |         # ``wrapper.starTicker()`` currently cashes ticker instances | ||||||
|  | @ -558,14 +581,17 @@ class Client: | ||||||
|         # XXX UPDATE: we can probably do the tick/trades scraping |         # XXX UPDATE: we can probably do the tick/trades scraping | ||||||
|         # inside our eventkit handler instead to bypass this entirely? |         # inside our eventkit handler instead to bypass this entirely? | ||||||
| 
 | 
 | ||||||
|  |         currency = '' | ||||||
|  | 
 | ||||||
|         # fqsn parsing stage |         # fqsn parsing stage | ||||||
|         # ------------------ |         # ------------------ | ||||||
|         if '.ib' in pattern: |         if '.ib' in pattern: | ||||||
|             from ..data._source import unpack_fqsn |             from ..data._source import unpack_fqsn | ||||||
|             broker, symbol, expiry = unpack_fqsn(pattern) |             _, symbol, expiry = unpack_fqsn(pattern) | ||||||
| 
 | 
 | ||||||
|         else: |         else: | ||||||
|             symbol = pattern |             symbol = pattern | ||||||
|  |             expiry = '' | ||||||
| 
 | 
 | ||||||
|         # another hack for forex pairs lul. |         # another hack for forex pairs lul. | ||||||
|         if ( |         if ( | ||||||
|  | @ -579,6 +605,7 @@ class Client: | ||||||
|                 symbol, currency = symbol.split('/') |                 symbol, currency = symbol.split('/') | ||||||
| 
 | 
 | ||||||
|         else: |         else: | ||||||
|  |             # TODO: yes, a cache.. | ||||||
|             # try: |             # try: | ||||||
|             #     # give the cache a go |             #     # give the cache a go | ||||||
|             #     return self._contracts[symbol] |             #     return self._contracts[symbol] | ||||||
|  | @ -589,9 +616,32 @@ class Client: | ||||||
|                 symbol, _, expiry = symbol.rpartition('.') |                 symbol, _, expiry = symbol.rpartition('.') | ||||||
| 
 | 
 | ||||||
|             # use heuristics to figure out contract "type" |             # use heuristics to figure out contract "type" | ||||||
|             sym, exch = symbol.upper().rsplit('.', maxsplit=1) |             symbol, exch = symbol.upper().rsplit('.', maxsplit=1) | ||||||
| 
 | 
 | ||||||
|         qualify: bool = True |         return symbol, currency, exch, expiry | ||||||
|  | 
 | ||||||
|  |     async def find_contracts( | ||||||
|  |         self, | ||||||
|  |         pattern: Optional[str] = None, | ||||||
|  |         contract: Optional[Contract] = None, | ||||||
|  |         qualify: bool = True, | ||||||
|  |         err_on_qualify: bool = True, | ||||||
|  | 
 | ||||||
|  |     ) -> Contract: | ||||||
|  | 
 | ||||||
|  |         if pattern is not None: | ||||||
|  |             symbol, currency, exch, expiry = self.parse_patt2fqsn( | ||||||
|  |                 pattern, | ||||||
|  |             ) | ||||||
|  |             sectype = '' | ||||||
|  | 
 | ||||||
|  |         else: | ||||||
|  |             assert contract | ||||||
|  |             symbol = contract.symbol | ||||||
|  |             sectype = contract.secType | ||||||
|  |             exch = contract.exchange or contract.primaryExchange | ||||||
|  |             expiry = contract.lastTradeDateOrContractMonth | ||||||
|  |             currency = contract.currency | ||||||
| 
 | 
 | ||||||
|         # contract searching stage |         # contract searching stage | ||||||
|         # ------------------------ |         # ------------------------ | ||||||
|  | @ -600,26 +650,27 @@ class Client: | ||||||
|         if exch in _futes_venues: |         if exch in _futes_venues: | ||||||
|             if expiry: |             if expiry: | ||||||
|                 # get the "front" contract |                 # get the "front" contract | ||||||
|                 contract = await self.get_fute( |                 con = await self.get_fute( | ||||||
|                     symbol=sym, |                     symbol=symbol, | ||||||
|                     exchange=exch, |                     exchange=exch, | ||||||
|                     expiry=expiry, |                     expiry=expiry, | ||||||
|                 ) |                 ) | ||||||
| 
 | 
 | ||||||
|             else: |             else: | ||||||
|                 # get the "front" contract |                 # get the "front" contract | ||||||
|                 contract = await self.get_fute( |                 con = await self.get_fute( | ||||||
|                     symbol=sym, |                     symbol=symbol, | ||||||
|                     exchange=exch, |                     exchange=exch, | ||||||
|                     front=True, |                     front=True, | ||||||
|                 ) |                 ) | ||||||
| 
 | 
 | ||||||
|             qualify = False |         elif ( | ||||||
| 
 |             exch in ('FOREX') | ||||||
|         elif exch in ('FOREX'): |             or sectype == 'CASH' | ||||||
|  |         ): | ||||||
|             # if '/' in symbol: |             # if '/' in symbol: | ||||||
|             #     currency = '' |             #     currency = '' | ||||||
|             #     symbol, currency = sym.split('/') |             #     symbol, currency = symbol.split('/') | ||||||
|             con = ibis.Forex( |             con = ibis.Forex( | ||||||
|                 pair=''.join((symbol, currency)), |                 pair=''.join((symbol, currency)), | ||||||
|                 currency=currency, |                 currency=currency, | ||||||
|  | @ -628,7 +679,7 @@ class Client: | ||||||
| 
 | 
 | ||||||
|         # commodities |         # commodities | ||||||
|         elif exch == 'CMDTY':  # eg. XAUUSD.CMDTY |         elif exch == 'CMDTY':  # eg. XAUUSD.CMDTY | ||||||
|             con_kwargs, bars_kwargs = _adhoc_symbol_map[sym] |             con_kwargs, bars_kwargs = _adhoc_symbol_map[symbol] | ||||||
|             con = ibis.Commodity(**con_kwargs) |             con = ibis.Commodity(**con_kwargs) | ||||||
|             con.bars_kwargs = bars_kwargs |             con.bars_kwargs = bars_kwargs | ||||||
| 
 | 
 | ||||||
|  | @ -650,29 +701,44 @@ class Client: | ||||||
|                 exch = 'SMART' |                 exch = 'SMART' | ||||||
| 
 | 
 | ||||||
|             con = ibis.Stock( |             con = ibis.Stock( | ||||||
|                 symbol=sym, |                 symbol=symbol, | ||||||
|                 exchange=exch, |                 exchange=exch, | ||||||
|                 primaryExchange=primaryExchange, |                 primaryExchange=primaryExchange, | ||||||
|                 currency=currency, |                 currency=currency, | ||||||
|             ) |             ) | ||||||
|         try: |  | ||||||
|             exch = 'SMART' if not exch else exch |             exch = 'SMART' if not exch else exch | ||||||
|             if qualify: |  | ||||||
|                 contract = (await self.ib.qualifyContractsAsync(con))[0] |  | ||||||
|             else: |  | ||||||
|                 assert contract |  | ||||||
| 
 | 
 | ||||||
|         except IndexError: |         contracts = [con] | ||||||
|             raise ValueError(f"No contract could be found {con}") |         if qualify: | ||||||
|  |             try: | ||||||
|  |                 contracts = await self.ib.qualifyContractsAsync(con) | ||||||
|  |             except RequestError as err: | ||||||
|  |                 msg = err.message | ||||||
|  |                 if ( | ||||||
|  |                     'No security definition' in msg | ||||||
|  |                     and not err_on_qualify | ||||||
|  |                 ): | ||||||
|  |                     log.warning( | ||||||
|  |                         f'Could not find def for {con}') | ||||||
|  |                     return None | ||||||
| 
 | 
 | ||||||
|         self._contracts[pattern] = contract |                 else: | ||||||
|  |                     raise | ||||||
|  |             if not contracts: | ||||||
|  |                 raise ValueError(f"No contract could be found {con}") | ||||||
| 
 | 
 | ||||||
|         # add an aditional entry with expiry suffix if available |         # pack all contracts into cache | ||||||
|         conexp = contract.lastTradeDateOrContractMonth |         for tract in contracts: | ||||||
|         if conexp: |             exch: str = tract.primaryExchange or tract.exchange or exch | ||||||
|             self._contracts[pattern + f'.{conexp}'] = contract |             pattern = f'{symbol}.{exch}' | ||||||
|  |             expiry = tract.lastTradeDateOrContractMonth | ||||||
|  |             # add an entry with expiry suffix if available | ||||||
|  |             if expiry: | ||||||
|  |                 pattern += f'.{expiry}' | ||||||
| 
 | 
 | ||||||
|         return contract |             self._contracts[pattern.lower()] = tract | ||||||
|  | 
 | ||||||
|  |         return contracts | ||||||
| 
 | 
 | ||||||
|     async def get_head_time( |     async def get_head_time( | ||||||
|         self, |         self, | ||||||
|  | @ -694,7 +760,7 @@ class Client: | ||||||
| 
 | 
 | ||||||
|     ) -> tuple[Contract, Ticker, ContractDetails]: |     ) -> tuple[Contract, Ticker, ContractDetails]: | ||||||
| 
 | 
 | ||||||
|         contract = await self.find_contract(symbol) |         contract = (await self.find_contracts(symbol))[0] | ||||||
|         ticker: Ticker = self.ib.reqMktData( |         ticker: Ticker = self.ib.reqMktData( | ||||||
|             contract, |             contract, | ||||||
|             snapshot=True, |             snapshot=True, | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue