commit
						bf7a49c19b
					
				| 
						 | 
				
			
			@ -29,6 +29,7 @@ import itertools
 | 
			
		|||
from math import isnan
 | 
			
		||||
from typing import (
 | 
			
		||||
    Any,
 | 
			
		||||
    Optional,
 | 
			
		||||
    Union,
 | 
			
		||||
)
 | 
			
		||||
import asyncio
 | 
			
		||||
| 
						 | 
				
			
			@ -43,8 +44,11 @@ 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,
 | 
			
		||||
    Option,
 | 
			
		||||
)
 | 
			
		||||
from ib_insync.order import Order
 | 
			
		||||
from ib_insync.ticker import Ticker
 | 
			
		||||
from ib_insync.objects import (
 | 
			
		||||
| 
						 | 
				
			
			@ -53,7 +57,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 +191,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
 | 
			
		||||
| 
						 | 
				
			
			@ -205,6 +212,19 @@ _adhoc_futes_set = {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# taken from list here:
 | 
			
		||||
# https://www.interactivebrokers.com/en/trading/products-spot-currencies.php
 | 
			
		||||
_adhoc_fiat_set = set((
 | 
			
		||||
    'USD, AED, AUD, CAD,'
 | 
			
		||||
    'CHF, CNH, CZK, DKK,'
 | 
			
		||||
    'EUR, GBP, HKD, HUF,'
 | 
			
		||||
    'ILS, JPY, MXN, NOK,'
 | 
			
		||||
    'NZD, PLN, RUB, SAR,'
 | 
			
		||||
    'SEK, SGD, TRY, ZAR'
 | 
			
		||||
    ).split(' ,')
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# map of symbols to contract ids
 | 
			
		||||
_adhoc_symbol_map = {
 | 
			
		||||
    # https://misc.interactivebrokers.com/cstools/contract_info/v3.10/index.php?action=Conid%20Info&wlId=IB&conid=69067924
 | 
			
		||||
| 
						 | 
				
			
			@ -234,6 +254,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
 | 
			
		||||
| 
						 | 
				
			
			@ -336,7 +357,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)
 | 
			
		||||
| 
						 | 
				
			
			@ -391,7 +412,15 @@ class Client:
 | 
			
		|||
                futs.append(self.ib.reqContractDetailsAsync(con))
 | 
			
		||||
 | 
			
		||||
        # 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
 | 
			
		||||
        details = {}
 | 
			
		||||
| 
						 | 
				
			
			@ -400,20 +429,11 @@ class Client:
 | 
			
		|||
            # XXX: if there is more then one entry in the details list
 | 
			
		||||
            # then the contract is so called "ambiguous".
 | 
			
		||||
            for d in details_set:
 | 
			
		||||
                con = d.contract
 | 
			
		||||
 | 
			
		||||
                key = '.'.join([
 | 
			
		||||
                    con.symbol,
 | 
			
		||||
                    con.primaryExchange or con.exchange,
 | 
			
		||||
                ])
 | 
			
		||||
                expiry = con.lastTradeDateOrContractMonth
 | 
			
		||||
                if expiry:
 | 
			
		||||
                    key += f'.{expiry}'
 | 
			
		||||
 | 
			
		||||
                # nested dataclass we probably don't need and that
 | 
			
		||||
                # won't IPC serialize..
 | 
			
		||||
                # nested dataclass we probably don't need and that won't
 | 
			
		||||
                # IPC serialize..
 | 
			
		||||
                d.secIdList = ''
 | 
			
		||||
 | 
			
		||||
                key, calc_price = con2fqsn(d.contract)
 | 
			
		||||
                details[key] = d
 | 
			
		||||
 | 
			
		||||
        return details
 | 
			
		||||
| 
						 | 
				
			
			@ -443,7 +463,7 @@ class Client:
 | 
			
		|||
        self,
 | 
			
		||||
        pattern: str,
 | 
			
		||||
        # how many contracts to search "up to"
 | 
			
		||||
        upto: int = 3,
 | 
			
		||||
        upto: int = 6,
 | 
			
		||||
        asdicts: bool = True,
 | 
			
		||||
 | 
			
		||||
    ) -> dict[str, ContractDetails]:
 | 
			
		||||
| 
						 | 
				
			
			@ -454,7 +474,6 @@ class Client:
 | 
			
		|||
            pattern,
 | 
			
		||||
            upto=upto,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        for key, deats in results.copy().items():
 | 
			
		||||
 | 
			
		||||
            tract = deats.contract
 | 
			
		||||
| 
						 | 
				
			
			@ -464,21 +483,44 @@ 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:
 | 
			
		||||
                        all_deats = await self.con_deats([con])
 | 
			
		||||
                        results |= all_deats
 | 
			
		||||
                    # 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('.')
 | 
			
		||||
                pair_key = "/".join([dst, src])
 | 
			
		||||
                exch = tract.exchange.lower()
 | 
			
		||||
                results[f'{pair_key}.{exch}'] = tract
 | 
			
		||||
                results.pop(key)
 | 
			
		||||
 | 
			
		||||
                # XXX: again seems to trigger the weird tractor
 | 
			
		||||
                # bug with the debugger..
 | 
			
		||||
                # assert 0
 | 
			
		||||
 | 
			
		||||
        return results
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -518,13 +560,11 @@ class Client:
 | 
			
		|||
            ibis.Contract(conId=conid)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    async def find_contract(
 | 
			
		||||
    def parse_patt2fqsn(
 | 
			
		||||
        self,
 | 
			
		||||
        pattern: str,
 | 
			
		||||
        currency: str = 'USD',
 | 
			
		||||
        **kwargs,
 | 
			
		||||
 | 
			
		||||
    ) -> Contract:
 | 
			
		||||
    ) -> tuple[str, str, str, str]:
 | 
			
		||||
 | 
			
		||||
        # TODO: we can't use this currently because
 | 
			
		||||
        # ``wrapper.starTicker()`` currently cashes ticker instances
 | 
			
		||||
| 
						 | 
				
			
			@ -537,58 +577,104 @@ 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 = ''
 | 
			
		||||
 | 
			
		||||
        # try:
 | 
			
		||||
        #     # give the cache a go
 | 
			
		||||
        #     return self._contracts[symbol]
 | 
			
		||||
        # except KeyError:
 | 
			
		||||
        #     log.debug(f'Looking up contract for {symbol}')
 | 
			
		||||
        expiry: str = ''
 | 
			
		||||
        if symbol.count('.') > 1:
 | 
			
		||||
            symbol, _, expiry = symbol.rpartition('.')
 | 
			
		||||
        # another hack for forex pairs lul.
 | 
			
		||||
        if (
 | 
			
		||||
            '.idealpro' in symbol
 | 
			
		||||
            # or '/' in symbol
 | 
			
		||||
        ):
 | 
			
		||||
            exch = 'IDEALPRO'
 | 
			
		||||
            symbol = symbol.removesuffix('.idealpro')
 | 
			
		||||
            if '/' in symbol:
 | 
			
		||||
                symbol, currency = symbol.split('/')
 | 
			
		||||
 | 
			
		||||
        # use heuristics to figure out contract "type"
 | 
			
		||||
        sym, exch = symbol.upper().rsplit('.', maxsplit=1)
 | 
			
		||||
        else:
 | 
			
		||||
            # TODO: yes, a cache..
 | 
			
		||||
            # try:
 | 
			
		||||
            #     # give the cache a go
 | 
			
		||||
            #     return self._contracts[symbol]
 | 
			
		||||
            # except KeyError:
 | 
			
		||||
            #     log.debug(f'Looking up contract for {symbol}')
 | 
			
		||||
            expiry: str = ''
 | 
			
		||||
            if symbol.count('.') > 1:
 | 
			
		||||
                symbol, _, expiry = symbol.rpartition('.')
 | 
			
		||||
 | 
			
		||||
        qualify: bool = True
 | 
			
		||||
            # use heuristics to figure out contract "type"
 | 
			
		||||
            symbol, exch = symbol.upper().rsplit('.', maxsplit=1)
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
        # ------------------------
 | 
			
		||||
 | 
			
		||||
        # futes
 | 
			
		||||
        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'):
 | 
			
		||||
            currency = ''
 | 
			
		||||
            symbol, currency = sym.split('/')
 | 
			
		||||
        elif (
 | 
			
		||||
            exch in ('IDEALPRO')
 | 
			
		||||
            or sectype == 'CASH'
 | 
			
		||||
        ):
 | 
			
		||||
            # if '/' in symbol:
 | 
			
		||||
            #     currency = ''
 | 
			
		||||
            #     symbol, currency = symbol.split('/')
 | 
			
		||||
            con = ibis.Forex(
 | 
			
		||||
                symbol=symbol,
 | 
			
		||||
                pair=''.join((symbol, currency)),
 | 
			
		||||
                currency=currency,
 | 
			
		||||
            )
 | 
			
		||||
            con.bars_kwargs = {'whatToShow': 'MIDPOINT'}
 | 
			
		||||
 | 
			
		||||
        # 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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -604,33 +690,50 @@ class Client:
 | 
			
		|||
                exch = 'SMART'
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
                exch = 'SMART'
 | 
			
		||||
                # XXX: order is super important here since
 | 
			
		||||
                # a primary == 'SMART' won't ever work.
 | 
			
		||||
                primaryExchange = exch
 | 
			
		||||
                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:
 | 
			
		||||
            raise ValueError(f"No contract could be found {con}")
 | 
			
		||||
        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
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
        conexp = contract.lastTradeDateOrContractMonth
 | 
			
		||||
        if conexp:
 | 
			
		||||
            self._contracts[pattern + f'.{conexp}'] = 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}'
 | 
			
		||||
 | 
			
		||||
        return contract
 | 
			
		||||
            self._contracts[pattern.lower()] = tract
 | 
			
		||||
 | 
			
		||||
        return contracts
 | 
			
		||||
 | 
			
		||||
    async def get_head_time(
 | 
			
		||||
        self,
 | 
			
		||||
| 
						 | 
				
			
			@ -649,9 +752,10 @@ class Client:
 | 
			
		|||
    async def get_sym_details(
 | 
			
		||||
        self,
 | 
			
		||||
        symbol: str,
 | 
			
		||||
 | 
			
		||||
    ) -> 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,
 | 
			
		||||
| 
						 | 
				
			
			@ -839,6 +943,73 @@ class Client:
 | 
			
		|||
        return self.ib.positions(account=account)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def con2fqsn(
 | 
			
		||||
    con: Contract,
 | 
			
		||||
    _cache: dict[int, (str, bool)] = {}
 | 
			
		||||
 | 
			
		||||
) -> tuple[str, bool]:
 | 
			
		||||
    '''
 | 
			
		||||
    Convert contracts to fqsn-style strings to be used both in symbol-search
 | 
			
		||||
    matching and as feed tokens passed to the front end data deed layer.
 | 
			
		||||
 | 
			
		||||
    Previously seen contracts are cached by id.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    # should be real volume for this contract by default
 | 
			
		||||
    calc_price = False
 | 
			
		||||
    if con.conId:
 | 
			
		||||
        try:
 | 
			
		||||
            return _cache[con.conId]
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            pass
 | 
			
		||||
 | 
			
		||||
    suffix = con.primaryExchange or con.exchange
 | 
			
		||||
    symbol = con.symbol
 | 
			
		||||
    expiry = con.lastTradeDateOrContractMonth or ''
 | 
			
		||||
 | 
			
		||||
    match con:
 | 
			
		||||
        case Option():
 | 
			
		||||
            # TODO: option symbol parsing and sane display:
 | 
			
		||||
            symbol = con.localSymbol.replace(' ', '')
 | 
			
		||||
 | 
			
		||||
        case ibis.Commodity():
 | 
			
		||||
            # commodities and forex don't have an exchange name and
 | 
			
		||||
            # no real volume so we have to calculate the price
 | 
			
		||||
            suffix = con.secType
 | 
			
		||||
 | 
			
		||||
            # no real volume on this tract
 | 
			
		||||
            calc_price = True
 | 
			
		||||
 | 
			
		||||
        case ibis.Forex() | ibis.Contract(secType='CASH'):
 | 
			
		||||
            dst, src = con.localSymbol.split('.')
 | 
			
		||||
            symbol = ''.join([dst, src])
 | 
			
		||||
            suffix = con.exchange
 | 
			
		||||
 | 
			
		||||
            # no real volume on forex feeds..
 | 
			
		||||
            calc_price = True
 | 
			
		||||
 | 
			
		||||
    if not suffix:
 | 
			
		||||
        entry = _adhoc_symbol_map.get(
 | 
			
		||||
            con.symbol or con.localSymbol
 | 
			
		||||
        )
 | 
			
		||||
        if entry:
 | 
			
		||||
            meta, kwargs = entry
 | 
			
		||||
            cid = meta.get('conId')
 | 
			
		||||
            if cid:
 | 
			
		||||
                assert con.conId == meta['conId']
 | 
			
		||||
            suffix = meta['exchange']
 | 
			
		||||
 | 
			
		||||
    # append a `.<suffix>` to the returned symbol
 | 
			
		||||
    # key for derivatives that normally is the expiry
 | 
			
		||||
    # date key.
 | 
			
		||||
    if expiry:
 | 
			
		||||
        suffix += f'.{expiry}'
 | 
			
		||||
 | 
			
		||||
    fqsn_key = '.'.join((symbol, suffix)).lower()
 | 
			
		||||
    _cache[con.conId] = fqsn_key, calc_price
 | 
			
		||||
    return fqsn_key, calc_price
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# per-actor API ep caching
 | 
			
		||||
_client_cache: dict[tuple[str, int], Client] = {}
 | 
			
		||||
_scan_ignore: set[tuple[str, int]] = set()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -36,6 +36,7 @@ import tractor
 | 
			
		|||
from ib_insync.contract import (
 | 
			
		||||
    Contract,
 | 
			
		||||
    Option,
 | 
			
		||||
    Forex,
 | 
			
		||||
)
 | 
			
		||||
from ib_insync.order import (
 | 
			
		||||
    Trade,
 | 
			
		||||
| 
						 | 
				
			
			@ -88,20 +89,29 @@ def pack_position(
 | 
			
		|||
        # TODO: lookup fqsn even for derivs.
 | 
			
		||||
        symbol = con.symbol.lower()
 | 
			
		||||
 | 
			
		||||
    # TODO: probably write a mofo exchange mapper routine since ib
 | 
			
		||||
    # can't get it's shit together like, ever.
 | 
			
		||||
 | 
			
		||||
    # try our best to figure out the exchange / venue
 | 
			
		||||
    exch = (con.primaryExchange or con.exchange).lower()
 | 
			
		||||
    if not exch:
 | 
			
		||||
        # for wtv cucked reason some futes don't show their
 | 
			
		||||
        # exchange (like CL.NYMEX) ...
 | 
			
		||||
        entry = _adhoc_symbol_map.get(
 | 
			
		||||
            con.symbol or con.localSymbol
 | 
			
		||||
        )
 | 
			
		||||
        if entry:
 | 
			
		||||
            meta, kwargs = entry
 | 
			
		||||
            cid = meta.get('conId')
 | 
			
		||||
            if cid:
 | 
			
		||||
                assert con.conId == meta['conId']
 | 
			
		||||
            exch = meta['exchange']
 | 
			
		||||
 | 
			
		||||
        if isinstance(con, Forex):
 | 
			
		||||
            # bc apparently it's not in the contract obj?
 | 
			
		||||
            exch = 'idealfx'
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            # for wtv cucked reason some futes don't show their
 | 
			
		||||
            # exchange (like CL.NYMEX) ...
 | 
			
		||||
            entry = _adhoc_symbol_map.get(
 | 
			
		||||
                con.symbol or con.localSymbol
 | 
			
		||||
            )
 | 
			
		||||
            if entry:
 | 
			
		||||
                meta, kwargs = entry
 | 
			
		||||
                cid = meta.get('conId')
 | 
			
		||||
                if cid:
 | 
			
		||||
                    assert con.conId == meta['conId']
 | 
			
		||||
                exch = meta['exchange']
 | 
			
		||||
 | 
			
		||||
    assert exch, f'No clue:\n {con}'
 | 
			
		||||
    fqsn = '.'.join((symbol, exch))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,7 +41,8 @@ from trio_typing import TaskStatus
 | 
			
		|||
from piker.data._sharedmem import ShmArray
 | 
			
		||||
from .._util import SymbolNotFound, NoData
 | 
			
		||||
from .api import (
 | 
			
		||||
    _adhoc_futes_set,
 | 
			
		||||
    # _adhoc_futes_set,
 | 
			
		||||
    con2fqsn,
 | 
			
		||||
    log,
 | 
			
		||||
    load_aio_clients,
 | 
			
		||||
    ibis,
 | 
			
		||||
| 
						 | 
				
			
			@ -207,8 +208,6 @@ async def get_bars(
 | 
			
		|||
 | 
			
		||||
        except RequestError as err:
 | 
			
		||||
            msg = err.message
 | 
			
		||||
            # why do we always need to rebind this?
 | 
			
		||||
            # _err = err
 | 
			
		||||
 | 
			
		||||
            if 'No market data permissions for' in msg:
 | 
			
		||||
                # TODO: signalling for no permissions searches
 | 
			
		||||
| 
						 | 
				
			
			@ -239,7 +238,8 @@ async def get_bars(
 | 
			
		|||
 | 
			
		||||
            # elif (
 | 
			
		||||
            #     err.code == 162 and
 | 
			
		||||
            #     'Trading TWS session is connected from a different IP address' in err.message
 | 
			
		||||
            #     'Trading TWS session is connected from a different IP
 | 
			
		||||
            #     address' in err.message
 | 
			
		||||
            # ):
 | 
			
		||||
            #     log.warning("ignoring ip address warning")
 | 
			
		||||
            #     continue
 | 
			
		||||
| 
						 | 
				
			
			@ -560,38 +560,18 @@ async def open_aio_quote_stream(
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
# TODO: cython/mypyc/numba this!
 | 
			
		||||
# or we can at least cache a majority of the values
 | 
			
		||||
# except for the ones we expect to change?..
 | 
			
		||||
def normalize(
 | 
			
		||||
    ticker: Ticker,
 | 
			
		||||
    calc_price: bool = False
 | 
			
		||||
 | 
			
		||||
) -> dict:
 | 
			
		||||
 | 
			
		||||
    # should be real volume for this contract by default
 | 
			
		||||
    calc_price = False
 | 
			
		||||
 | 
			
		||||
    # check for special contract types
 | 
			
		||||
    con = ticker.contract
 | 
			
		||||
    if type(con) in (
 | 
			
		||||
        ibis.Commodity,
 | 
			
		||||
        ibis.Forex,
 | 
			
		||||
    ):
 | 
			
		||||
        # commodities and forex don't have an exchange name and
 | 
			
		||||
        # no real volume so we have to calculate the price
 | 
			
		||||
        suffix = con.secType
 | 
			
		||||
        # no real volume on this tract
 | 
			
		||||
        calc_price = True
 | 
			
		||||
 | 
			
		||||
    else:
 | 
			
		||||
        suffix = con.primaryExchange
 | 
			
		||||
        if not suffix:
 | 
			
		||||
            suffix = con.exchange
 | 
			
		||||
 | 
			
		||||
        # append a `.<suffix>` to the returned symbol
 | 
			
		||||
        # key for derivatives that normally is the expiry
 | 
			
		||||
        # date key.
 | 
			
		||||
        expiry = con.lastTradeDateOrContractMonth
 | 
			
		||||
        if expiry:
 | 
			
		||||
            suffix += f'.{expiry}'
 | 
			
		||||
    fqsn, calc_price = con2fqsn(con)
 | 
			
		||||
 | 
			
		||||
    # convert named tuples to dicts so we send usable keys
 | 
			
		||||
    new_ticks = []
 | 
			
		||||
| 
						 | 
				
			
			@ -623,9 +603,7 @@ def normalize(
 | 
			
		|||
 | 
			
		||||
    # generate fqsn with possible specialized suffix
 | 
			
		||||
    # for derivatives, note the lowercase.
 | 
			
		||||
    data['symbol'] = data['fqsn'] = '.'.join(
 | 
			
		||||
        (con.symbol, suffix)
 | 
			
		||||
    ).lower()
 | 
			
		||||
    data['symbol'] = data['fqsn'] = fqsn
 | 
			
		||||
 | 
			
		||||
    # convert named tuples to dicts for transport
 | 
			
		||||
    tbts = data.get('tickByTicks')
 | 
			
		||||
| 
						 | 
				
			
			@ -690,6 +668,13 @@ async def stream_quotes(
 | 
			
		|||
            # TODO: more consistent field translation
 | 
			
		||||
            atype = syminfo['asset_type'] = asset_type_map[syminfo['secType']]
 | 
			
		||||
 | 
			
		||||
            if atype in {
 | 
			
		||||
                'forex',
 | 
			
		||||
                'index',
 | 
			
		||||
                'commodity',
 | 
			
		||||
            }:
 | 
			
		||||
                syminfo['no_vlm'] = True
 | 
			
		||||
 | 
			
		||||
            # for stocks it seems TWS reports too small a tick size
 | 
			
		||||
            # such that you can't submit orders with that granularity?
 | 
			
		||||
            min_tick = 0.01 if atype == 'stock' else 0
 | 
			
		||||
| 
						 | 
				
			
			@ -716,9 +701,9 @@ async def stream_quotes(
 | 
			
		|||
                },
 | 
			
		||||
 | 
			
		||||
            }
 | 
			
		||||
            return init_msgs
 | 
			
		||||
            return init_msgs, syminfo
 | 
			
		||||
 | 
			
		||||
        init_msgs = mk_init_msgs()
 | 
			
		||||
        init_msgs, syminfo = mk_init_msgs()
 | 
			
		||||
 | 
			
		||||
        # TODO: we should instead spawn a task that waits on a feed to start
 | 
			
		||||
        # and let it wait indefinitely..instead of this hard coded stuff.
 | 
			
		||||
| 
						 | 
				
			
			@ -727,7 +712,13 @@ async def stream_quotes(
 | 
			
		|||
 | 
			
		||||
        # it might be outside regular trading hours so see if we can at
 | 
			
		||||
        # least grab history.
 | 
			
		||||
        if isnan(first_ticker.last):
 | 
			
		||||
        if (
 | 
			
		||||
            isnan(first_ticker.last)
 | 
			
		||||
            and type(first_ticker.contract) not in (
 | 
			
		||||
                ibis.Commodity,
 | 
			
		||||
                ibis.Forex
 | 
			
		||||
            )
 | 
			
		||||
        ):
 | 
			
		||||
            task_status.started((init_msgs, first_quote))
 | 
			
		||||
 | 
			
		||||
            # it's not really live but this will unblock
 | 
			
		||||
| 
						 | 
				
			
			@ -750,10 +741,16 @@ async def stream_quotes(
 | 
			
		|||
            task_status.started((init_msgs, first_quote))
 | 
			
		||||
 | 
			
		||||
            async with aclosing(stream):
 | 
			
		||||
                if type(first_ticker.contract) not in (
 | 
			
		||||
                    ibis.Commodity,
 | 
			
		||||
                    ibis.Forex
 | 
			
		||||
                ):
 | 
			
		||||
                if syminfo.get('no_vlm', False):
 | 
			
		||||
 | 
			
		||||
                    # generally speaking these feeds don't
 | 
			
		||||
                    # include vlm data.
 | 
			
		||||
                    atype = syminfo['asset_type']
 | 
			
		||||
                    log.info(
 | 
			
		||||
                        f'Non-vlm asset {sym}@{atype}, skipping quote poll...'
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                else:
 | 
			
		||||
                    # wait for real volume on feed (trading might be closed)
 | 
			
		||||
                    while True:
 | 
			
		||||
                        ticker = await stream.receive()
 | 
			
		||||
| 
						 | 
				
			
			@ -812,6 +809,9 @@ async def data_reset_hack(
 | 
			
		|||
          successful.
 | 
			
		||||
        - other OS support?
 | 
			
		||||
        - integration with ``ib-gw`` run in docker + Xorg?
 | 
			
		||||
        - is it possible to offer a local server that can be accessed by
 | 
			
		||||
          a client? Would be sure be handy for running native java blobs
 | 
			
		||||
          that need to be wrangle.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -926,7 +926,8 @@ async def open_symbol_search(
 | 
			
		|||
                    # adhoc_match_results = {}
 | 
			
		||||
                    # if adhoc_matches:
 | 
			
		||||
                    #     # TODO: do we need to pull contract details?
 | 
			
		||||
                    #     adhoc_match_results = {i[0]: {} for i in adhoc_matches}
 | 
			
		||||
                    #     adhoc_match_results = {i[0]: {} for i in
 | 
			
		||||
                    #     adhoc_matches}
 | 
			
		||||
 | 
			
		||||
                log.debug(f'fuzzy matching stocks {stock_results}')
 | 
			
		||||
                stock_matches = fuzzy.extractBests(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -56,7 +56,7 @@ def iterticks(
 | 
			
		|||
                    sig = (
 | 
			
		||||
                        time,
 | 
			
		||||
                        tick['price'],
 | 
			
		||||
                        tick['size']
 | 
			
		||||
                        tick.get('size')
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                    if ttype == 'dark_trade':
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -453,13 +453,6 @@ class LinkedSplits(QWidget):
 | 
			
		|||
        # add crosshair graphic
 | 
			
		||||
        self.chart.addItem(self.cursor)
 | 
			
		||||
 | 
			
		||||
        # axis placement
 | 
			
		||||
        if (
 | 
			
		||||
            _xaxis_at == 'bottom' and
 | 
			
		||||
            'bottom' in self.chart.plotItem.axes
 | 
			
		||||
        ):
 | 
			
		||||
            self.chart.hideAxis('bottom')
 | 
			
		||||
 | 
			
		||||
        # style?
 | 
			
		||||
        self.chart.setFrameStyle(
 | 
			
		||||
            QFrame.StyledPanel |
 | 
			
		||||
| 
						 | 
				
			
			@ -524,21 +517,26 @@ class LinkedSplits(QWidget):
 | 
			
		|||
        cpw.hideAxis('left')
 | 
			
		||||
        cpw.hideAxis('bottom')
 | 
			
		||||
 | 
			
		||||
        if self.xaxis_chart:
 | 
			
		||||
            self.xaxis_chart.hideAxis('bottom')
 | 
			
		||||
        if (
 | 
			
		||||
            _xaxis_at == 'bottom' and (
 | 
			
		||||
                self.xaxis_chart
 | 
			
		||||
                or (
 | 
			
		||||
                    not self.subplots
 | 
			
		||||
                    and self.xaxis_chart is None
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        ):
 | 
			
		||||
            if self.xaxis_chart:
 | 
			
		||||
                self.xaxis_chart.hideAxis('bottom')
 | 
			
		||||
 | 
			
		||||
            # presuming we only want it at the true bottom of all charts.
 | 
			
		||||
            # XXX: uses new api from our ``pyqtgraph`` fork.
 | 
			
		||||
            # https://github.com/pikers/pyqtgraph/tree/plotitemoverlay_onto_pg_master
 | 
			
		||||
            # _ = self.xaxis_chart.removeAxis('bottom', unlink=False)
 | 
			
		||||
            # assert 'bottom' not in self.xaxis_chart.plotItem.axes
 | 
			
		||||
 | 
			
		||||
            self.xaxis_chart = cpw
 | 
			
		||||
            cpw.showAxis('bottom')
 | 
			
		||||
 | 
			
		||||
        if self.xaxis_chart is None:
 | 
			
		||||
            self.xaxis_chart = cpw
 | 
			
		||||
 | 
			
		||||
        qframe.chart = cpw
 | 
			
		||||
        qframe.hbox.addWidget(cpw)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -63,7 +63,7 @@ from ..log import get_logger
 | 
			
		|||
log = get_logger(__name__)
 | 
			
		||||
 | 
			
		||||
# TODO: load this from a config.toml!
 | 
			
		||||
_quote_throttle_rate: int = 22  # Hz
 | 
			
		||||
_quote_throttle_rate: int = 60  # Hz
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# a working tick-type-classes template
 | 
			
		||||
| 
						 | 
				
			
			@ -136,16 +136,16 @@ class DisplayState:
 | 
			
		|||
    # high level chart handles
 | 
			
		||||
    linked: LinkedSplits
 | 
			
		||||
    chart: ChartPlotWidget
 | 
			
		||||
    vlm_chart: ChartPlotWidget
 | 
			
		||||
 | 
			
		||||
    # axis labels
 | 
			
		||||
    l1: L1Labels
 | 
			
		||||
    last_price_sticky: YAxisLabel
 | 
			
		||||
    vlm_sticky: YAxisLabel
 | 
			
		||||
 | 
			
		||||
    # misc state tracking
 | 
			
		||||
    vars: dict[str, Any]
 | 
			
		||||
 | 
			
		||||
    vlm_chart: Optional[ChartPlotWidget] = None
 | 
			
		||||
    vlm_sticky: Optional[YAxisLabel] = None
 | 
			
		||||
    wap_in_history: bool = False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -185,9 +185,6 @@ async def graphics_update_loop(
 | 
			
		|||
        *ohlcv.array[-1][['index', 'close']]
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if vlm_chart:
 | 
			
		||||
        vlm_sticky = vlm_chart._ysticks['volume']
 | 
			
		||||
 | 
			
		||||
    maxmin = partial(
 | 
			
		||||
        chart_maxmin,
 | 
			
		||||
        chart,
 | 
			
		||||
| 
						 | 
				
			
			@ -236,8 +233,6 @@ async def graphics_update_loop(
 | 
			
		|||
        'ohlcv': ohlcv,
 | 
			
		||||
        'chart': chart,
 | 
			
		||||
        'last_price_sticky': last_price_sticky,
 | 
			
		||||
        'vlm_chart': vlm_chart,
 | 
			
		||||
        'vlm_sticky': vlm_sticky,
 | 
			
		||||
        'l1': l1,
 | 
			
		||||
 | 
			
		||||
        'vars': {
 | 
			
		||||
| 
						 | 
				
			
			@ -250,6 +245,11 @@ async def graphics_update_loop(
 | 
			
		|||
        }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    if vlm_chart:
 | 
			
		||||
        vlm_sticky = vlm_chart._ysticks['volume']
 | 
			
		||||
        ds.vlm_chart = vlm_chart
 | 
			
		||||
        ds.vlm_sticky = vlm_sticky
 | 
			
		||||
 | 
			
		||||
    chart.default_view()
 | 
			
		||||
 | 
			
		||||
    # main real-time quotes update loop
 | 
			
		||||
| 
						 | 
				
			
			@ -322,7 +322,7 @@ def graphics_update_cycle(
 | 
			
		|||
    for sym, quote in ds.quotes.items():
 | 
			
		||||
 | 
			
		||||
        # compute the first available graphic's x-units-per-pixel
 | 
			
		||||
        uppx = vlm_chart.view.x_uppx()
 | 
			
		||||
        uppx = chart.view.x_uppx()
 | 
			
		||||
 | 
			
		||||
        # NOTE: vlm may be written by the ``brokerd`` backend
 | 
			
		||||
        # event though a tick sample is not emitted.
 | 
			
		||||
| 
						 | 
				
			
			@ -786,7 +786,10 @@ async def display_symbol_data(
 | 
			
		|||
        async with trio.open_nursery() as ln:
 | 
			
		||||
 | 
			
		||||
            # if available load volume related built-in display(s)
 | 
			
		||||
            if has_vlm(ohlcv):
 | 
			
		||||
            if (
 | 
			
		||||
                not symbol.broker_info[provider].get('no_vlm', False)
 | 
			
		||||
                and has_vlm(ohlcv)
 | 
			
		||||
            ):
 | 
			
		||||
                vlm_chart = await ln.start(
 | 
			
		||||
                    open_vlm_displays,
 | 
			
		||||
                    linked,
 | 
			
		||||
| 
						 | 
				
			
			@ -821,6 +824,9 @@ async def display_symbol_data(
 | 
			
		|||
                    order_mode_started
 | 
			
		||||
                )
 | 
			
		||||
            ):
 | 
			
		||||
                if not vlm_chart:
 | 
			
		||||
                    chart.default_view()
 | 
			
		||||
 | 
			
		||||
                # let Qt run to render all widgets and make sure the
 | 
			
		||||
                # sidepanes line up vertically.
 | 
			
		||||
                await trio.sleep(0)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue