commit
bf7a49c19b
|
@ -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,11 @@ 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,
|
||||||
|
Option,
|
||||||
|
)
|
||||||
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 +57,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 +191,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
|
||||||
|
@ -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
|
# map of symbols to contract ids
|
||||||
_adhoc_symbol_map = {
|
_adhoc_symbol_map = {
|
||||||
# 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
|
||||||
|
@ -234,6 +254,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
|
||||||
|
@ -336,7 +357,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)
|
||||||
|
@ -391,7 +412,15 @@ 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 = {}
|
||||||
|
@ -400,20 +429,11 @@ class Client:
|
||||||
# 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:
|
||||||
con = d.contract
|
|
||||||
|
|
||||||
key = '.'.join([
|
# nested dataclass we probably don't need and that won't
|
||||||
con.symbol,
|
# IPC serialize..
|
||||||
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..
|
|
||||||
d.secIdList = ''
|
d.secIdList = ''
|
||||||
|
key, calc_price = con2fqsn(d.contract)
|
||||||
details[key] = d
|
details[key] = d
|
||||||
|
|
||||||
return details
|
return details
|
||||||
|
@ -443,7 +463,7 @@ class Client:
|
||||||
self,
|
self,
|
||||||
pattern: str,
|
pattern: str,
|
||||||
# how many contracts to search "up to"
|
# how many contracts to search "up to"
|
||||||
upto: int = 3,
|
upto: int = 6,
|
||||||
asdicts: bool = True,
|
asdicts: bool = True,
|
||||||
|
|
||||||
) -> dict[str, ContractDetails]:
|
) -> dict[str, ContractDetails]:
|
||||||
|
@ -454,7 +474,6 @@ class Client:
|
||||||
pattern,
|
pattern,
|
||||||
upto=upto,
|
upto=upto,
|
||||||
)
|
)
|
||||||
|
|
||||||
for key, deats in results.copy().items():
|
for key, deats in results.copy().items():
|
||||||
|
|
||||||
tract = deats.contract
|
tract = deats.contract
|
||||||
|
@ -464,21 +483,44 @@ 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:
|
# forex pairs
|
||||||
log.warning(err.message)
|
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
|
return results
|
||||||
|
|
||||||
|
@ -518,13 +560,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 = 'USD',
|
|
||||||
**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
|
||||||
|
@ -537,12 +577,30 @@ 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
|
||||||
|
# ------------------
|
||||||
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.
|
||||||
|
if (
|
||||||
|
'.idealpro' in symbol
|
||||||
|
# or '/' in symbol
|
||||||
|
):
|
||||||
|
exch = 'IDEALPRO'
|
||||||
|
symbol = symbol.removesuffix('.idealpro')
|
||||||
|
if '/' in symbol:
|
||||||
|
symbol, currency = symbol.split('/')
|
||||||
|
|
||||||
|
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]
|
||||||
|
@ -553,42 +611,70 @@ 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
|
||||||
|
# ------------------------
|
||||||
|
|
||||||
# futes
|
# futes
|
||||||
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 ('IDEALPRO')
|
||||||
elif exch in ('FOREX'):
|
or sectype == 'CASH'
|
||||||
currency = ''
|
):
|
||||||
symbol, currency = sym.split('/')
|
# if '/' in symbol:
|
||||||
|
# currency = ''
|
||||||
|
# symbol, currency = symbol.split('/')
|
||||||
con = ibis.Forex(
|
con = ibis.Forex(
|
||||||
symbol=symbol,
|
pair=''.join((symbol, currency)),
|
||||||
currency=currency,
|
currency=currency,
|
||||||
)
|
)
|
||||||
con.bars_kwargs = {'whatToShow': 'MIDPOINT'}
|
con.bars_kwargs = {'whatToShow': 'MIDPOINT'}
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
|
@ -604,33 +690,50 @@ class Client:
|
||||||
exch = 'SMART'
|
exch = 'SMART'
|
||||||
|
|
||||||
else:
|
else:
|
||||||
exch = 'SMART'
|
# XXX: order is super important here since
|
||||||
|
# a primary == 'SMART' won't ever work.
|
||||||
primaryExchange = exch
|
primaryExchange = exch
|
||||||
|
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,
|
||||||
|
@ -649,9 +752,10 @@ class Client:
|
||||||
async def get_sym_details(
|
async def get_sym_details(
|
||||||
self,
|
self,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
|
|
||||||
) -> 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,
|
||||||
|
@ -839,6 +943,73 @@ class Client:
|
||||||
return self.ib.positions(account=account)
|
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
|
# per-actor API ep caching
|
||||||
_client_cache: dict[tuple[str, int], Client] = {}
|
_client_cache: dict[tuple[str, int], Client] = {}
|
||||||
_scan_ignore: set[tuple[str, int]] = set()
|
_scan_ignore: set[tuple[str, int]] = set()
|
||||||
|
|
|
@ -36,6 +36,7 @@ import tractor
|
||||||
from ib_insync.contract import (
|
from ib_insync.contract import (
|
||||||
Contract,
|
Contract,
|
||||||
Option,
|
Option,
|
||||||
|
Forex,
|
||||||
)
|
)
|
||||||
from ib_insync.order import (
|
from ib_insync.order import (
|
||||||
Trade,
|
Trade,
|
||||||
|
@ -88,9 +89,18 @@ def pack_position(
|
||||||
# TODO: lookup fqsn even for derivs.
|
# TODO: lookup fqsn even for derivs.
|
||||||
symbol = con.symbol.lower()
|
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
|
# try our best to figure out the exchange / venue
|
||||||
exch = (con.primaryExchange or con.exchange).lower()
|
exch = (con.primaryExchange or con.exchange).lower()
|
||||||
if not exch:
|
if not exch:
|
||||||
|
|
||||||
|
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
|
# for wtv cucked reason some futes don't show their
|
||||||
# exchange (like CL.NYMEX) ...
|
# exchange (like CL.NYMEX) ...
|
||||||
entry = _adhoc_symbol_map.get(
|
entry = _adhoc_symbol_map.get(
|
||||||
|
|
|
@ -41,7 +41,8 @@ from trio_typing import TaskStatus
|
||||||
from piker.data._sharedmem import ShmArray
|
from piker.data._sharedmem import ShmArray
|
||||||
from .._util import SymbolNotFound, NoData
|
from .._util import SymbolNotFound, NoData
|
||||||
from .api import (
|
from .api import (
|
||||||
_adhoc_futes_set,
|
# _adhoc_futes_set,
|
||||||
|
con2fqsn,
|
||||||
log,
|
log,
|
||||||
load_aio_clients,
|
load_aio_clients,
|
||||||
ibis,
|
ibis,
|
||||||
|
@ -207,8 +208,6 @@ async def get_bars(
|
||||||
|
|
||||||
except RequestError as err:
|
except RequestError as err:
|
||||||
msg = err.message
|
msg = err.message
|
||||||
# why do we always need to rebind this?
|
|
||||||
# _err = err
|
|
||||||
|
|
||||||
if 'No market data permissions for' in msg:
|
if 'No market data permissions for' in msg:
|
||||||
# TODO: signalling for no permissions searches
|
# TODO: signalling for no permissions searches
|
||||||
|
@ -239,7 +238,8 @@ async def get_bars(
|
||||||
|
|
||||||
# elif (
|
# elif (
|
||||||
# err.code == 162 and
|
# 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")
|
# log.warning("ignoring ip address warning")
|
||||||
# continue
|
# continue
|
||||||
|
@ -560,38 +560,18 @@ async def open_aio_quote_stream(
|
||||||
|
|
||||||
|
|
||||||
# TODO: cython/mypyc/numba this!
|
# 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(
|
def normalize(
|
||||||
ticker: Ticker,
|
ticker: Ticker,
|
||||||
calc_price: bool = False
|
calc_price: bool = False
|
||||||
|
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
|
||||||
# should be real volume for this contract by default
|
|
||||||
calc_price = False
|
|
||||||
|
|
||||||
# check for special contract types
|
# check for special contract types
|
||||||
con = ticker.contract
|
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:
|
fqsn, calc_price = con2fqsn(con)
|
||||||
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}'
|
|
||||||
|
|
||||||
# convert named tuples to dicts so we send usable keys
|
# convert named tuples to dicts so we send usable keys
|
||||||
new_ticks = []
|
new_ticks = []
|
||||||
|
@ -623,9 +603,7 @@ def normalize(
|
||||||
|
|
||||||
# generate fqsn with possible specialized suffix
|
# generate fqsn with possible specialized suffix
|
||||||
# for derivatives, note the lowercase.
|
# for derivatives, note the lowercase.
|
||||||
data['symbol'] = data['fqsn'] = '.'.join(
|
data['symbol'] = data['fqsn'] = fqsn
|
||||||
(con.symbol, suffix)
|
|
||||||
).lower()
|
|
||||||
|
|
||||||
# convert named tuples to dicts for transport
|
# convert named tuples to dicts for transport
|
||||||
tbts = data.get('tickByTicks')
|
tbts = data.get('tickByTicks')
|
||||||
|
@ -690,6 +668,13 @@ async def stream_quotes(
|
||||||
# TODO: more consistent field translation
|
# TODO: more consistent field translation
|
||||||
atype = syminfo['asset_type'] = asset_type_map[syminfo['secType']]
|
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
|
# for stocks it seems TWS reports too small a tick size
|
||||||
# such that you can't submit orders with that granularity?
|
# such that you can't submit orders with that granularity?
|
||||||
min_tick = 0.01 if atype == 'stock' else 0
|
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
|
# 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.
|
# 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
|
# it might be outside regular trading hours so see if we can at
|
||||||
# least grab history.
|
# 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))
|
task_status.started((init_msgs, first_quote))
|
||||||
|
|
||||||
# it's not really live but this will unblock
|
# it's not really live but this will unblock
|
||||||
|
@ -750,10 +741,16 @@ async def stream_quotes(
|
||||||
task_status.started((init_msgs, first_quote))
|
task_status.started((init_msgs, first_quote))
|
||||||
|
|
||||||
async with aclosing(stream):
|
async with aclosing(stream):
|
||||||
if type(first_ticker.contract) not in (
|
if syminfo.get('no_vlm', False):
|
||||||
ibis.Commodity,
|
|
||||||
ibis.Forex
|
# 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)
|
# wait for real volume on feed (trading might be closed)
|
||||||
while True:
|
while True:
|
||||||
ticker = await stream.receive()
|
ticker = await stream.receive()
|
||||||
|
@ -812,6 +809,9 @@ async def data_reset_hack(
|
||||||
successful.
|
successful.
|
||||||
- other OS support?
|
- other OS support?
|
||||||
- integration with ``ib-gw`` run in docker + Xorg?
|
- 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 = {}
|
# adhoc_match_results = {}
|
||||||
# if adhoc_matches:
|
# if adhoc_matches:
|
||||||
# # TODO: do we need to pull contract details?
|
# # 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}')
|
log.debug(f'fuzzy matching stocks {stock_results}')
|
||||||
stock_matches = fuzzy.extractBests(
|
stock_matches = fuzzy.extractBests(
|
||||||
|
|
|
@ -56,7 +56,7 @@ def iterticks(
|
||||||
sig = (
|
sig = (
|
||||||
time,
|
time,
|
||||||
tick['price'],
|
tick['price'],
|
||||||
tick['size']
|
tick.get('size')
|
||||||
)
|
)
|
||||||
|
|
||||||
if ttype == 'dark_trade':
|
if ttype == 'dark_trade':
|
||||||
|
|
|
@ -453,13 +453,6 @@ class LinkedSplits(QWidget):
|
||||||
# add crosshair graphic
|
# add crosshair graphic
|
||||||
self.chart.addItem(self.cursor)
|
self.chart.addItem(self.cursor)
|
||||||
|
|
||||||
# axis placement
|
|
||||||
if (
|
|
||||||
_xaxis_at == 'bottom' and
|
|
||||||
'bottom' in self.chart.plotItem.axes
|
|
||||||
):
|
|
||||||
self.chart.hideAxis('bottom')
|
|
||||||
|
|
||||||
# style?
|
# style?
|
||||||
self.chart.setFrameStyle(
|
self.chart.setFrameStyle(
|
||||||
QFrame.StyledPanel |
|
QFrame.StyledPanel |
|
||||||
|
@ -524,6 +517,15 @@ class LinkedSplits(QWidget):
|
||||||
cpw.hideAxis('left')
|
cpw.hideAxis('left')
|
||||||
cpw.hideAxis('bottom')
|
cpw.hideAxis('bottom')
|
||||||
|
|
||||||
|
if (
|
||||||
|
_xaxis_at == 'bottom' and (
|
||||||
|
self.xaxis_chart
|
||||||
|
or (
|
||||||
|
not self.subplots
|
||||||
|
and self.xaxis_chart is None
|
||||||
|
)
|
||||||
|
)
|
||||||
|
):
|
||||||
if self.xaxis_chart:
|
if self.xaxis_chart:
|
||||||
self.xaxis_chart.hideAxis('bottom')
|
self.xaxis_chart.hideAxis('bottom')
|
||||||
|
|
||||||
|
@ -532,13 +534,9 @@ class LinkedSplits(QWidget):
|
||||||
# https://github.com/pikers/pyqtgraph/tree/plotitemoverlay_onto_pg_master
|
# https://github.com/pikers/pyqtgraph/tree/plotitemoverlay_onto_pg_master
|
||||||
# _ = self.xaxis_chart.removeAxis('bottom', unlink=False)
|
# _ = self.xaxis_chart.removeAxis('bottom', unlink=False)
|
||||||
# assert 'bottom' not in self.xaxis_chart.plotItem.axes
|
# assert 'bottom' not in self.xaxis_chart.plotItem.axes
|
||||||
|
|
||||||
self.xaxis_chart = cpw
|
self.xaxis_chart = cpw
|
||||||
cpw.showAxis('bottom')
|
cpw.showAxis('bottom')
|
||||||
|
|
||||||
if self.xaxis_chart is None:
|
|
||||||
self.xaxis_chart = cpw
|
|
||||||
|
|
||||||
qframe.chart = cpw
|
qframe.chart = cpw
|
||||||
qframe.hbox.addWidget(cpw)
|
qframe.hbox.addWidget(cpw)
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,7 @@ from ..log import get_logger
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
# TODO: load this from a config.toml!
|
# 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
|
# a working tick-type-classes template
|
||||||
|
@ -136,16 +136,16 @@ class DisplayState:
|
||||||
# high level chart handles
|
# high level chart handles
|
||||||
linked: LinkedSplits
|
linked: LinkedSplits
|
||||||
chart: ChartPlotWidget
|
chart: ChartPlotWidget
|
||||||
vlm_chart: ChartPlotWidget
|
|
||||||
|
|
||||||
# axis labels
|
# axis labels
|
||||||
l1: L1Labels
|
l1: L1Labels
|
||||||
last_price_sticky: YAxisLabel
|
last_price_sticky: YAxisLabel
|
||||||
vlm_sticky: YAxisLabel
|
|
||||||
|
|
||||||
# misc state tracking
|
# misc state tracking
|
||||||
vars: dict[str, Any]
|
vars: dict[str, Any]
|
||||||
|
|
||||||
|
vlm_chart: Optional[ChartPlotWidget] = None
|
||||||
|
vlm_sticky: Optional[YAxisLabel] = None
|
||||||
wap_in_history: bool = False
|
wap_in_history: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
@ -185,9 +185,6 @@ async def graphics_update_loop(
|
||||||
*ohlcv.array[-1][['index', 'close']]
|
*ohlcv.array[-1][['index', 'close']]
|
||||||
)
|
)
|
||||||
|
|
||||||
if vlm_chart:
|
|
||||||
vlm_sticky = vlm_chart._ysticks['volume']
|
|
||||||
|
|
||||||
maxmin = partial(
|
maxmin = partial(
|
||||||
chart_maxmin,
|
chart_maxmin,
|
||||||
chart,
|
chart,
|
||||||
|
@ -236,8 +233,6 @@ async def graphics_update_loop(
|
||||||
'ohlcv': ohlcv,
|
'ohlcv': ohlcv,
|
||||||
'chart': chart,
|
'chart': chart,
|
||||||
'last_price_sticky': last_price_sticky,
|
'last_price_sticky': last_price_sticky,
|
||||||
'vlm_chart': vlm_chart,
|
|
||||||
'vlm_sticky': vlm_sticky,
|
|
||||||
'l1': l1,
|
'l1': l1,
|
||||||
|
|
||||||
'vars': {
|
'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()
|
chart.default_view()
|
||||||
|
|
||||||
# main real-time quotes update loop
|
# main real-time quotes update loop
|
||||||
|
@ -322,7 +322,7 @@ def graphics_update_cycle(
|
||||||
for sym, quote in ds.quotes.items():
|
for sym, quote in ds.quotes.items():
|
||||||
|
|
||||||
# compute the first available graphic's x-units-per-pixel
|
# 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
|
# NOTE: vlm may be written by the ``brokerd`` backend
|
||||||
# event though a tick sample is not emitted.
|
# event though a tick sample is not emitted.
|
||||||
|
@ -786,7 +786,10 @@ async def display_symbol_data(
|
||||||
async with trio.open_nursery() as ln:
|
async with trio.open_nursery() as ln:
|
||||||
|
|
||||||
# if available load volume related built-in display(s)
|
# 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(
|
vlm_chart = await ln.start(
|
||||||
open_vlm_displays,
|
open_vlm_displays,
|
||||||
linked,
|
linked,
|
||||||
|
@ -821,6 +824,9 @@ async def display_symbol_data(
|
||||||
order_mode_started
|
order_mode_started
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
|
if not vlm_chart:
|
||||||
|
chart.default_view()
|
||||||
|
|
||||||
# let Qt run to render all widgets and make sure the
|
# let Qt run to render all widgets and make sure the
|
||||||
# sidepanes line up vertically.
|
# sidepanes line up vertically.
|
||||||
await trio.sleep(0)
|
await trio.sleep(0)
|
||||||
|
|
Loading…
Reference in New Issue