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
|
||||||
|
try:
|
||||||
results = await asyncio.gather(*futs)
|
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,22 +491,33 @@ 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
|
||||||
|
# with the qualify flag.
|
||||||
|
# cons = await self.find_contracts(
|
||||||
|
# contract=con,
|
||||||
|
# err_on_qualify=False,
|
||||||
|
# )
|
||||||
|
# if cons:
|
||||||
all_deats = await self.con_deats([con])
|
all_deats = await self.con_deats([con])
|
||||||
results |= all_deats
|
results |= all_deats
|
||||||
|
|
||||||
except RequestError as err:
|
|
||||||
log.warning(err.message)
|
|
||||||
|
|
||||||
# forex pairs
|
# forex pairs
|
||||||
elif sectype == 'CASH':
|
elif sectype == 'CASH':
|
||||||
dst, src = tract.localSymbol.split('.')
|
dst, src = tract.localSymbol.split('.')
|
||||||
|
@ -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]
|
||||||
|
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}")
|
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
|
self._contracts[pattern.lower()] = tract
|
||||||
conexp = contract.lastTradeDateOrContractMonth
|
|
||||||
if conexp:
|
|
||||||
self._contracts[pattern + f'.{conexp}'] = contract
|
|
||||||
|
|
||||||
return contract
|
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