Merge pull request #358 from pikers/fix_forex

Fix forex
historical_breakeven_pp_price
goodboy 2022-07-21 17:52:08 -04:00 committed by GitHub
commit bf7a49c19b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 332 additions and 146 deletions

View File

@ -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()

View File

@ -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(

View File

@ -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(

View File

@ -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':

View File

@ -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)

View File

@ -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)