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
Tyler Goodlet 2022-07-15 15:27:51 -04:00
parent 0580b204a3
commit 0ef5da0881
1 changed files with 115 additions and 49 deletions

View File

@ -29,6 +29,7 @@ import itertools
from math import isnan
from typing import (
Any,
Optional,
Union,
)
import asyncio
@ -43,8 +44,10 @@ import trio
import tractor
from tractor import to_asyncio
import ib_insync as ibis
from ib_insync.wrapper import RequestError
from ib_insync.contract import Contract, ContractDetails
from ib_insync.contract import (
Contract,
ContractDetails,
)
from ib_insync.order import Order
from ib_insync.ticker import Ticker
from ib_insync.objects import (
@ -53,7 +56,10 @@ from ib_insync.objects import (
Execution,
CommissionReport,
)
from ib_insync.wrapper import Wrapper
from ib_insync.wrapper import (
Wrapper,
RequestError,
)
from ib_insync.client import Client as ib_Client
import numpy as np
@ -184,12 +190,12 @@ _adhoc_futes_set = {
'ethusdrr.cmecrypto',
# agriculture
'he.globex', # lean hogs
'le.globex', # live cattle (geezers)
'gf.globex', # feeder cattle (younguns)
'he.nymex', # lean hogs
'le.nymex', # live cattle (geezers)
'gf.nymex', # feeder cattle (younguns)
# raw
'lb.globex', # random len lumber
'lb.nymex', # random len lumber
# metals
'xauusd.cmdty', # gold spot
@ -247,6 +253,7 @@ _exch_skip_list = {
'VALUE',
'FUNDSERV',
'SWB2',
'PSE',
}
# 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
contract = await self.find_contract(fqsn)
contract = (await self.find_contracts(fqsn))[0]
bars_kwargs.update(getattr(contract, 'bars_kwargs', {}))
# _min = min(2000*100, count)
@ -404,12 +411,19 @@ class Client:
futs.append(self.ib.reqContractDetailsAsync(con))
# batch request all details
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
details = {}
for details_set in results:
# XXX: if there is more then one entry in the details list
# then the contract is so called "ambiguous".
for d in details_set:
@ -477,22 +491,33 @@ class Client:
if sectype == 'IND':
results[f'{sym}.IND'] = tract
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,
# https://interactivebrokers.github.io/tws-api/basic_contracts.html#fut
con = ibis.Future(
symbol=sym,
exchange=exch,
)
try:
# TODO: make this work, think it's something to do
# with the qualify flag.
# cons = await self.find_contracts(
# contract=con,
# err_on_qualify=False,
# )
# if cons:
all_deats = await self.con_deats([con])
results |= all_deats
except RequestError as err:
log.warning(err.message)
# forex pairs
elif sectype == 'CASH':
dst, src = tract.localSymbol.split('.')
@ -539,13 +564,11 @@ class Client:
ibis.Contract(conId=conid)
)
async def find_contract(
def parse_patt2fqsn(
self,
pattern: str,
currency: str = '',
**kwargs,
) -> Contract:
) -> tuple[str, str, str, str]:
# TODO: we can't use this currently because
# ``wrapper.starTicker()`` currently cashes ticker instances
@ -558,14 +581,17 @@ class Client:
# XXX UPDATE: we can probably do the tick/trades scraping
# inside our eventkit handler instead to bypass this entirely?
currency = ''
# fqsn parsing stage
# ------------------
if '.ib' in pattern:
from ..data._source import unpack_fqsn
broker, symbol, expiry = unpack_fqsn(pattern)
_, symbol, expiry = unpack_fqsn(pattern)
else:
symbol = pattern
expiry = ''
# another hack for forex pairs lul.
if (
@ -579,6 +605,7 @@ class Client:
symbol, currency = symbol.split('/')
else:
# TODO: yes, a cache..
# try:
# # give the cache a go
# return self._contracts[symbol]
@ -589,9 +616,32 @@ class Client:
symbol, _, expiry = symbol.rpartition('.')
# 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
# ------------------------
@ -600,26 +650,27 @@ class Client:
if exch in _futes_venues:
if expiry:
# get the "front" contract
contract = await self.get_fute(
symbol=sym,
con = await self.get_fute(
symbol=symbol,
exchange=exch,
expiry=expiry,
)
else:
# get the "front" contract
contract = await self.get_fute(
symbol=sym,
con = await self.get_fute(
symbol=symbol,
exchange=exch,
front=True,
)
qualify = False
elif exch in ('FOREX'):
elif (
exch in ('FOREX')
or sectype == 'CASH'
):
# if '/' in symbol:
# currency = ''
# symbol, currency = sym.split('/')
# symbol, currency = symbol.split('/')
con = ibis.Forex(
pair=''.join((symbol, currency)),
currency=currency,
@ -628,7 +679,7 @@ class Client:
# commodities
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.bars_kwargs = bars_kwargs
@ -650,29 +701,44 @@ class Client:
exch = 'SMART'
con = ibis.Stock(
symbol=sym,
symbol=symbol,
exchange=exch,
primaryExchange=primaryExchange,
currency=currency,
)
try:
exch = 'SMART' if not exch else exch
if qualify:
contract = (await self.ib.qualifyContractsAsync(con))[0]
else:
assert contract
except IndexError:
contracts = [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
else:
raise
if not contracts:
raise ValueError(f"No contract could be found {con}")
self._contracts[pattern] = contract
# pack all contracts into cache
for tract in contracts:
exch: str = tract.primaryExchange or tract.exchange or exch
pattern = f'{symbol}.{exch}'
expiry = tract.lastTradeDateOrContractMonth
# add an entry with expiry suffix if available
if expiry:
pattern += f'.{expiry}'
# add an aditional entry with expiry suffix if available
conexp = contract.lastTradeDateOrContractMonth
if conexp:
self._contracts[pattern + f'.{conexp}'] = contract
self._contracts[pattern.lower()] = tract
return contract
return contracts
async def get_head_time(
self,
@ -694,7 +760,7 @@ class Client:
) -> tuple[Contract, Ticker, ContractDetails]:
contract = await self.find_contract(symbol)
contract = (await self.find_contracts(symbol))[0]
ticker: Ticker = self.ib.reqMktData(
contract,
snapshot=True,