ib: rework client-internal contract caching

Add new `Client` attr tables to better stash `Contract` lookup results
normally mapped from some in put FQME;

- `._contracts: dict[str, Contract]` for any input pattern (fqme).
- `._cons: dict[str, Contract] = {}` for the `.conId: int` inputs.
- `_cons2mkts: bidict[Contract, MktPair]` for mapping back and forth
  between ib and piker internal pair types.

Further,
- type out as many ib_insync internal types as possible mostly for
  contract related objects.
- change `Client.trades()` -> `.get_fills()` and return directly the
  result from `IB.fill()`.
account_tests
Tyler Goodlet 2023-07-25 16:22:03 -04:00
parent 897c20bd4a
commit 50b221f788
1 changed files with 65 additions and 63 deletions

View File

@ -34,16 +34,15 @@ from functools import (
) )
import itertools import itertools
from math import isnan from math import isnan
from typing import (
Any,
Callable,
Optional,
Union,
)
import asyncio import asyncio
from pprint import pformat from pprint import pformat
import inspect import inspect
import time import time
from typing import (
Any,
Callable,
Union,
)
from types import SimpleNamespace from types import SimpleNamespace
from bidict import bidict from bidict import bidict
@ -56,26 +55,20 @@ from ib_insync import (
client as ib_client, client as ib_client,
IB, IB,
Contract, Contract,
ContractDetails,
Crypto, Crypto,
Commodity, Commodity,
Forex, Forex,
Future, Future,
ContFuture, ContFuture,
Stock, Stock,
) Order,
from ib_insync.contract import ( Ticker,
ContractDetails,
)
from ib_insync.order import Order
from ib_insync.ticker import Ticker
from ib_insync.objects import (
BarDataList, BarDataList,
Position, Position,
Fill, Fill,
Execution, # Execution,
CommissionReport, # CommissionReport,
)
from ib_insync.wrapper import (
Wrapper, Wrapper,
RequestError, RequestError,
) )
@ -85,6 +78,7 @@ import numpy as np
# non-relative for backends so that non-builting backends # non-relative for backends so that non-builting backends
# can be easily modelled after this style B) # can be easily modelled after this style B)
from piker import config from piker import config
from piker.accounting import MktPair
from .symbols import ( from .symbols import (
con2fqme, con2fqme,
parse_patt2fqme, parse_patt2fqme,
@ -264,7 +258,13 @@ class Client:
Note: this client requires running inside an ``asyncio`` loop. Note: this client requires running inside an ``asyncio`` loop.
''' '''
# keyed by fqmes
_contracts: dict[str, Contract] = {} _contracts: dict[str, Contract] = {}
# keyed by conId
_cons: dict[str, Contract] = {}
# for going between ib and piker types
_cons2mkts: bidict[Contract, MktPair] = bidict({})
def __init__( def __init__(
self, self,
@ -282,26 +282,16 @@ class Client:
self.ib = ib self.ib = ib
self.ib.RaiseRequestErrors: bool = True self.ib.RaiseRequestErrors: bool = True
# contract cache async def get_fills(self) -> list[Fill]:
self._cons: dict[str, Contract] = {}
async def trades(self) -> list[dict]:
''' '''
Return list of trade-fills from current session in ``dict``. Return list of rents `Fills` from trading session.
In theory this can be configured for dumping clears from multiple
days but can't member where to set that..
''' '''
norm_fills: list[dict] = []
fills: list[Fill] = self.ib.fills() fills: list[Fill] = self.ib.fills()
for fill in fills: return fills
fill = fill._asdict() # namedtuple
for key, val in fill.items():
match val:
case Contract() | Execution() | CommissionReport():
fill[key] = asdict(val)
norm_fills.append(fill)
return norm_fills
async def orders(self) -> list[Order]: async def orders(self) -> list[Order]:
return await self.ib.reqAllOpenOrdersAsync( return await self.ib.reqAllOpenOrdersAsync(
@ -347,7 +337,7 @@ class Client:
_enters += 1 _enters += 1
contract = (await self.find_contracts(fqme))[0] contract: Contract = (await self.find_contracts(fqme))[0]
bars_kwargs.update(getattr(contract, 'bars_kwargs', {})) bars_kwargs.update(getattr(contract, 'bars_kwargs', {}))
bars = await self.ib.reqHistoricalDataAsync( bars = await self.ib.reqHistoricalDataAsync(
@ -575,7 +565,8 @@ class Client:
) -> Contract: ) -> Contract:
''' '''
Get an unqualifed contract for the current "continous" future. Get an unqualifed contract for the current "continous"
future.
''' '''
# it's the "front" contract returned here # it's the "front" contract returned here
@ -606,13 +597,13 @@ class Client:
con: Contract = await self.ib.qualifyContractsAsync( con: Contract = await self.ib.qualifyContractsAsync(
Contract(conId=conid) Contract(conId=conid)
) )
self._cons[conid] = con self._cons[str(conid)] = con[0]
return con return con
async def find_contracts( async def find_contracts(
self, self,
pattern: Optional[str] = None, pattern: str | None = None,
contract: Optional[Contract] = None, contract: Contract | None = None,
qualify: bool = True, qualify: bool = True,
err_on_qualify: bool = True, err_on_qualify: bool = True,
@ -622,21 +613,23 @@ class Client:
symbol, currency, exch, expiry = parse_patt2fqme( symbol, currency, exch, expiry = parse_patt2fqme(
pattern, pattern,
) )
sectype = '' sectype: str = ''
exch: str = exch.upper()
else: else:
assert contract assert contract
symbol = contract.symbol symbol: str = contract.symbol
sectype = contract.secType sectype: str = contract.secType
exch = contract.exchange or contract.primaryExchange exch: str = contract.exchange or contract.primaryExchange
expiry = contract.lastTradeDateOrContractMonth expiry: str = contract.lastTradeDateOrContractMonth
currency = contract.currency currency: str = contract.currency
# contract searching stage # contract searching stage
# ------------------------ # ------------------------
# futes # futes, ensure exch/venue is uppercase for matching
if exch in _futes_venues: # our adhoc set.
if exch.upper() in _futes_venues:
if expiry: if expiry:
# get the "front" contract # get the "front" contract
con = await self.get_fute( con = await self.get_fute(
@ -704,10 +697,12 @@ class Client:
) )
exch = 'SMART' if not exch else exch exch = 'SMART' if not exch else exch
contracts = [con] contracts: list[Contract] = [con]
if qualify: if qualify:
try: try:
contracts = await self.ib.qualifyContractsAsync(con) contracts: list[Contract] = (
await self.ib.qualifyContractsAsync(con)
)
except RequestError as err: except RequestError as err:
msg = err.message msg = err.message
if ( if (
@ -725,14 +720,21 @@ class Client:
# pack all contracts into cache # pack all contracts into cache
for tract in contracts: for tract in contracts:
exch: str = tract.primaryExchange or tract.exchange or exch exch: str = (
pattern = f'{symbol}.{exch}' tract.primaryExchange
expiry = tract.lastTradeDateOrContractMonth or tract.exchange
or exch
)
pattern: str = f'{symbol}.{exch}'
expiry: str = tract.lastTradeDateOrContractMonth
# add an entry with expiry suffix if available # add an entry with expiry suffix if available
if expiry: if expiry:
pattern += f'.{expiry}' pattern += f'.{expiry}'
self._contracts[pattern.lower()] = tract # directly cache the input pattern to the output
# contract match as well as by the IB-internal conId.
self._contracts[pattern] = tract
self._cons[str(tract.conId)] = tract
return contracts return contracts
@ -755,21 +757,21 @@ class Client:
async def get_sym_details( async def get_sym_details(
self, self,
symbol: str, fqme: str,
) -> tuple[ ) -> tuple[
Contract, Contract,
ContractDetails, ContractDetails,
]: ]:
''' '''
Get summary (meta) data for a given symbol str including Return matching contracts for a given ``fqme: str`` including
``Contract`` and its details and a (first snapshot of the) ``Contract`` and matching ``ContractDetails``.
``Ticker``.
''' '''
contract = (await self.find_contracts(symbol))[0] contract: Contract = (await self.find_contracts(fqme))[0]
details_fute = self.ib.reqContractDetailsAsync(contract) details: ContractDetails = (
details = (await details_fute)[0] await self.ib.reqContractDetailsAsync(contract)
)[0]
return contract, details return contract, details
async def get_quote( async def get_quote(
@ -842,7 +844,7 @@ class Client:
''' '''
try: try:
contract = self._contracts[symbol] con: Contract = self._contracts[symbol]
except KeyError: except KeyError:
# require that the symbol has been previously cached by # require that the symbol has been previously cached by
# a data feed request - ensure we aren't making orders # a data feed request - ensure we aren't making orders
@ -851,7 +853,7 @@ class Client:
try: try:
trade = self.ib.placeOrder( trade = self.ib.placeOrder(
contract, con,
Order( Order(
orderId=reqid or 0, # stupid api devs.. orderId=reqid or 0, # stupid api devs..
action=action.upper(), # BUY/SELL action=action.upper(), # BUY/SELL
@ -908,7 +910,7 @@ class Client:
reqId: int, reqId: int,
errorCode: int, errorCode: int,
errorString: str, errorString: str,
contract: Contract, con: Contract,
) -> None: ) -> None:
@ -933,7 +935,7 @@ class Client:
'reqid': reqId, 'reqid': reqId,
'reason': reason, 'reason': reason,
'error_code': errorCode, 'error_code': errorCode,
'contract': contract, 'contract': con,
} }
)) ))
except trio.BrokenResourceError: except trio.BrokenResourceError: