Compare commits
17 Commits
gitea_feat
...
pre_multi_
Author | SHA1 | Date |
---|---|---|
|
9b6f4d24be | |
|
4eea8042ff | |
|
9fc45c2bff | |
|
e7751cb5dd | |
|
79b27899bf | |
|
312c1552cd | |
|
f1b5c6e62c | |
|
16699bdc88 | |
|
5151971131 | |
|
81f0fc77e3 | |
|
3e45a61287 | |
|
bc46f17fae | |
|
27749e90c3 | |
|
63c3d5ba74 | |
|
8caea80133 | |
|
7bc67671b6 | |
|
b11dfbb008 |
|
@ -62,39 +62,39 @@ services:
|
||||||
# - "127.0.0.1:4002:4002"
|
# - "127.0.0.1:4002:4002"
|
||||||
# - "127.0.0.1:5900:5900"
|
# - "127.0.0.1:5900:5900"
|
||||||
|
|
||||||
ib_gw_live:
|
# ib_gw_live:
|
||||||
image: waytrade/ib-gateway:1012.2i
|
# image: waytrade/ib-gateway:1012.2i
|
||||||
restart: always
|
# restart: always
|
||||||
network_mode: 'host'
|
# network_mode: 'host'
|
||||||
|
|
||||||
volumes:
|
# volumes:
|
||||||
- type: bind
|
# - type: bind
|
||||||
source: ./jts_live.ini
|
# source: ./jts_live.ini
|
||||||
target: /root/jts/jts.ini
|
# target: /root/jts/jts.ini
|
||||||
# don't let ibc clobber this file for
|
# # don't let ibc clobber this file for
|
||||||
# the main reason of not having a stupid
|
# # the main reason of not having a stupid
|
||||||
# timezone set..
|
# # timezone set..
|
||||||
read_only: true
|
# read_only: true
|
||||||
|
|
||||||
# force our own ibc config
|
# # force our own ibc config
|
||||||
- type: bind
|
# - type: bind
|
||||||
source: ./ibc.ini
|
# source: ./ibc.ini
|
||||||
target: /root/ibc/config.ini
|
# target: /root/ibc/config.ini
|
||||||
|
|
||||||
# force our noop script - socat isn't needed in host mode.
|
# # force our noop script - socat isn't needed in host mode.
|
||||||
- type: bind
|
# - type: bind
|
||||||
source: ./fork_ports_delayed.sh
|
# source: ./fork_ports_delayed.sh
|
||||||
target: /root/scripts/fork_ports_delayed.sh
|
# target: /root/scripts/fork_ports_delayed.sh
|
||||||
|
|
||||||
# force our noop script - socat isn't needed in host mode.
|
# # force our noop script - socat isn't needed in host mode.
|
||||||
- type: bind
|
# - type: bind
|
||||||
source: ./run_x11_vnc.sh
|
# source: ./run_x11_vnc.sh
|
||||||
target: /root/scripts/run_x11_vnc.sh
|
# target: /root/scripts/run_x11_vnc.sh
|
||||||
read_only: true
|
# read_only: true
|
||||||
|
|
||||||
# NOTE: to fill these out, define an `.env` file in the same dir as
|
# # NOTE: to fill these out, define an `.env` file in the same dir as
|
||||||
# this compose file which looks something like:
|
# # this compose file which looks something like:
|
||||||
environment:
|
# environment:
|
||||||
TRADING_MODE: 'live'
|
# TRADING_MODE: 'live'
|
||||||
VNC_SERVER_PASSWORD: 'doggy'
|
# VNC_SERVER_PASSWORD: 'doggy'
|
||||||
VNC_SERVER_PORT: '3004'
|
# VNC_SERVER_PORT: '3004'
|
||||||
|
|
|
@ -18,3 +18,10 @@
|
||||||
piker: trading gear for hackers.
|
piker: trading gear for hackers.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from ._daemon import open_piker_runtime
|
||||||
|
from .data.feed import open_feed
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'open_piker_runtime',
|
||||||
|
'open_feed',
|
||||||
|
]
|
||||||
|
|
|
@ -35,7 +35,12 @@ log = get_logger(__name__)
|
||||||
|
|
||||||
_root_dname = 'pikerd'
|
_root_dname = 'pikerd'
|
||||||
|
|
||||||
_registry_addr = ('127.0.0.1', 6116)
|
_registry_host: str = '127.0.0.1'
|
||||||
|
_registry_port: int = 6116
|
||||||
|
_registry_addr = (
|
||||||
|
_registry_host,
|
||||||
|
_registry_port,
|
||||||
|
)
|
||||||
_tractor_kwargs: dict[str, Any] = {
|
_tractor_kwargs: dict[str, Any] = {
|
||||||
# use a different registry addr then tractor's default
|
# use a different registry addr then tractor's default
|
||||||
'arbiter_addr': _registry_addr
|
'arbiter_addr': _registry_addr
|
||||||
|
@ -135,6 +140,7 @@ async def open_pikerd(
|
||||||
# XXX: you should pretty much never want debug mode
|
# XXX: you should pretty much never want debug mode
|
||||||
# for data daemons when running in production.
|
# for data daemons when running in production.
|
||||||
debug_mode: bool = False,
|
debug_mode: bool = False,
|
||||||
|
registry_addr: None | tuple[str, int] = None,
|
||||||
|
|
||||||
) -> Optional[tractor._portal.Portal]:
|
) -> Optional[tractor._portal.Portal]:
|
||||||
'''
|
'''
|
||||||
|
@ -153,7 +159,7 @@ async def open_pikerd(
|
||||||
tractor.open_root_actor(
|
tractor.open_root_actor(
|
||||||
|
|
||||||
# passed through to ``open_root_actor``
|
# passed through to ``open_root_actor``
|
||||||
arbiter_addr=_registry_addr,
|
arbiter_addr=registry_addr or _registry_addr,
|
||||||
name=_root_dname,
|
name=_root_dname,
|
||||||
loglevel=loglevel,
|
loglevel=loglevel,
|
||||||
debug_mode=debug_mode,
|
debug_mode=debug_mode,
|
||||||
|
@ -193,7 +199,7 @@ async def open_piker_runtime(
|
||||||
# for data daemons when running in production.
|
# for data daemons when running in production.
|
||||||
debug_mode: bool = False,
|
debug_mode: bool = False,
|
||||||
|
|
||||||
) -> Optional[tractor._portal.Portal]:
|
) -> tractor.Actor:
|
||||||
'''
|
'''
|
||||||
Start a piker actor who's runtime will automatically sync with
|
Start a piker actor who's runtime will automatically sync with
|
||||||
existing piker actors on the local link based on configuration.
|
existing piker actors on the local link based on configuration.
|
||||||
|
@ -248,6 +254,7 @@ async def maybe_open_runtime(
|
||||||
@acm
|
@acm
|
||||||
async def maybe_open_pikerd(
|
async def maybe_open_pikerd(
|
||||||
loglevel: Optional[str] = None,
|
loglevel: Optional[str] = None,
|
||||||
|
registry_addr: None | tuple = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
|
||||||
) -> Union[tractor._portal.Portal, Services]:
|
) -> Union[tractor._portal.Portal, Services]:
|
||||||
|
@ -260,11 +267,19 @@ async def maybe_open_pikerd(
|
||||||
get_console_log(loglevel)
|
get_console_log(loglevel)
|
||||||
|
|
||||||
# subtle, we must have the runtime up here or portal lookup will fail
|
# subtle, we must have the runtime up here or portal lookup will fail
|
||||||
async with maybe_open_runtime(loglevel, **kwargs):
|
async with (
|
||||||
|
maybe_open_runtime(loglevel, **kwargs),
|
||||||
async with tractor.find_actor(_root_dname) as portal:
|
tractor.find_actor(_root_dname) as portal
|
||||||
# assert portal is not None
|
):
|
||||||
if portal is not None:
|
# connect to any existing daemon presuming
|
||||||
|
# its registry socket was selected.
|
||||||
|
if (
|
||||||
|
portal is not None
|
||||||
|
and (
|
||||||
|
registry_addr is None
|
||||||
|
or portal.channel.raddr == registry_addr
|
||||||
|
)
|
||||||
|
):
|
||||||
yield portal
|
yield portal
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -274,6 +289,7 @@ async def maybe_open_pikerd(
|
||||||
|
|
||||||
loglevel=loglevel,
|
loglevel=loglevel,
|
||||||
debug_mode=kwargs.get('debug_mode', False),
|
debug_mode=kwargs.get('debug_mode', False),
|
||||||
|
registry_addr=registry_addr,
|
||||||
|
|
||||||
) as _:
|
) as _:
|
||||||
# in the case where we're starting up the
|
# in the case where we're starting up the
|
||||||
|
|
|
@ -371,8 +371,8 @@ async def update_and_audit_msgs(
|
||||||
else:
|
else:
|
||||||
entry = f'split_ratio = 1/{int(reverse_split_ratio)}'
|
entry = f'split_ratio = 1/{int(reverse_split_ratio)}'
|
||||||
|
|
||||||
raise ValueError(
|
# raise ValueError(
|
||||||
# log.error(
|
log.error(
|
||||||
f'POSITION MISMATCH ib <-> piker ledger:\n'
|
f'POSITION MISMATCH ib <-> piker ledger:\n'
|
||||||
f'ib: {ibppmsg}\n'
|
f'ib: {ibppmsg}\n'
|
||||||
f'piker: {msg}\n'
|
f'piker: {msg}\n'
|
||||||
|
@ -883,7 +883,7 @@ async def deliver_trade_events(
|
||||||
# execdict.pop('acctNumber')
|
# execdict.pop('acctNumber')
|
||||||
|
|
||||||
fill_msg = BrokerdFill(
|
fill_msg = BrokerdFill(
|
||||||
# should match the value returned from
|
# NOTE: should match the value returned from
|
||||||
# `.submit_limit()`
|
# `.submit_limit()`
|
||||||
reqid=execu.orderId,
|
reqid=execu.orderId,
|
||||||
time_ns=time.time_ns(), # cuz why not
|
time_ns=time.time_ns(), # cuz why not
|
||||||
|
|
|
@ -1038,7 +1038,13 @@ async def open_symbol_search(
|
||||||
stock_results = []
|
stock_results = []
|
||||||
|
|
||||||
async def stash_results(target: Awaitable[list]):
|
async def stash_results(target: Awaitable[list]):
|
||||||
stock_results.extend(await target)
|
try:
|
||||||
|
results = await target
|
||||||
|
except tractor.trionics.Lagged:
|
||||||
|
print("IB SYM-SEARCH OVERRUN?!?")
|
||||||
|
return
|
||||||
|
|
||||||
|
stock_results.extend(results)
|
||||||
|
|
||||||
for i in range(10):
|
for i in range(10):
|
||||||
with trio.move_on_after(3) as cs:
|
with trio.move_on_after(3) as cs:
|
||||||
|
|
|
@ -85,6 +85,7 @@ class Pair(Struct):
|
||||||
margin_call: str # margin call level
|
margin_call: str # margin call level
|
||||||
margin_stop: str # stop-out/liquidation margin level
|
margin_stop: str # stop-out/liquidation margin level
|
||||||
ordermin: float # minimum order volume for pair
|
ordermin: float # minimum order volume for pair
|
||||||
|
tick_size: float # min price step size
|
||||||
|
|
||||||
|
|
||||||
class OHLC(Struct):
|
class OHLC(Struct):
|
||||||
|
|
|
@ -1239,8 +1239,7 @@ async def process_client_order_cmds(
|
||||||
pred = mk_check(trigger_price, last, action)
|
pred = mk_check(trigger_price, last, action)
|
||||||
|
|
||||||
spread_slap: float = 5
|
spread_slap: float = 5
|
||||||
sym = fqsn.replace(f'.{brokers[0]}', '')
|
min_tick = feed.symbols[fqsn].tick_size
|
||||||
min_tick = feed.symbols[sym].tick_size
|
|
||||||
|
|
||||||
if action == 'buy':
|
if action == 'buy':
|
||||||
tickfilter = ('ask', 'last', 'trade')
|
tickfilter = ('ask', 'last', 'trade')
|
||||||
|
|
|
@ -27,7 +27,11 @@ import tractor
|
||||||
|
|
||||||
from ..log import get_console_log, get_logger, colorize_json
|
from ..log import get_console_log, get_logger, colorize_json
|
||||||
from ..brokers import get_brokermod
|
from ..brokers import get_brokermod
|
||||||
from .._daemon import _tractor_kwargs
|
from .._daemon import (
|
||||||
|
_tractor_kwargs,
|
||||||
|
_registry_host,
|
||||||
|
_registry_port,
|
||||||
|
)
|
||||||
from .. import config
|
from .. import config
|
||||||
|
|
||||||
|
|
||||||
|
@ -39,13 +43,21 @@ DEFAULT_BROKER = 'questrade'
|
||||||
@click.option('--loglevel', '-l', default='warning', help='Logging level')
|
@click.option('--loglevel', '-l', default='warning', help='Logging level')
|
||||||
@click.option('--tl', is_flag=True, help='Enable tractor logging')
|
@click.option('--tl', is_flag=True, help='Enable tractor logging')
|
||||||
@click.option('--pdb', is_flag=True, help='Enable tractor debug mode')
|
@click.option('--pdb', is_flag=True, help='Enable tractor debug mode')
|
||||||
@click.option('--host', '-h', default='127.0.0.1', help='Host address to bind')
|
@click.option('--host', '-h', default=None, help='Host addr to bind')
|
||||||
|
@click.option('--port', '-p', default=None, help='Port number to bind')
|
||||||
@click.option(
|
@click.option(
|
||||||
'--tsdb',
|
'--tsdb',
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
help='Enable local ``marketstore`` instance'
|
help='Enable local ``marketstore`` instance'
|
||||||
)
|
)
|
||||||
def pikerd(loglevel, host, tl, pdb, tsdb):
|
def pikerd(
|
||||||
|
loglevel: str,
|
||||||
|
host: str,
|
||||||
|
port: int,
|
||||||
|
tl: bool,
|
||||||
|
pdb: bool,
|
||||||
|
tsdb: bool,
|
||||||
|
):
|
||||||
'''
|
'''
|
||||||
Spawn the piker broker-daemon.
|
Spawn the piker broker-daemon.
|
||||||
|
|
||||||
|
@ -62,12 +74,21 @@ def pikerd(loglevel, host, tl, pdb, tsdb):
|
||||||
"\n"
|
"\n"
|
||||||
))
|
))
|
||||||
|
|
||||||
|
reg_addr: None | tuple[str, int] = None
|
||||||
|
if host or port:
|
||||||
|
reg_addr = (
|
||||||
|
host or _registry_host,
|
||||||
|
int(port) or _registry_port,
|
||||||
|
)
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
|
|
||||||
async with (
|
async with (
|
||||||
open_pikerd(
|
open_pikerd(
|
||||||
loglevel=loglevel,
|
loglevel=loglevel,
|
||||||
debug_mode=pdb,
|
debug_mode=pdb,
|
||||||
|
registry_addr=reg_addr,
|
||||||
|
|
||||||
), # normally delivers a ``Services`` handle
|
), # normally delivers a ``Services`` handle
|
||||||
trio.open_nursery() as n,
|
trio.open_nursery() as n,
|
||||||
):
|
):
|
||||||
|
@ -104,8 +125,19 @@ def pikerd(loglevel, host, tl, pdb, tsdb):
|
||||||
@click.option('--loglevel', '-l', default='warning', help='Logging level')
|
@click.option('--loglevel', '-l', default='warning', help='Logging level')
|
||||||
@click.option('--tl', is_flag=True, help='Enable tractor logging')
|
@click.option('--tl', is_flag=True, help='Enable tractor logging')
|
||||||
@click.option('--configdir', '-c', help='Configuration directory')
|
@click.option('--configdir', '-c', help='Configuration directory')
|
||||||
|
@click.option('--host', '-h', default=None, help='Host addr to bind')
|
||||||
|
@click.option('--port', '-p', default=None, help='Port number to bind')
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def cli(ctx, brokers, loglevel, tl, configdir):
|
def cli(
|
||||||
|
ctx: click.Context,
|
||||||
|
brokers: list[str],
|
||||||
|
loglevel: str,
|
||||||
|
tl: bool,
|
||||||
|
configdir: str,
|
||||||
|
host: str,
|
||||||
|
port: int,
|
||||||
|
|
||||||
|
) -> None:
|
||||||
if configdir is not None:
|
if configdir is not None:
|
||||||
assert os.path.isdir(configdir), f"`{configdir}` is not a valid path"
|
assert os.path.isdir(configdir), f"`{configdir}` is not a valid path"
|
||||||
config._override_config_dir(configdir)
|
config._override_config_dir(configdir)
|
||||||
|
@ -117,6 +149,13 @@ def cli(ctx, brokers, loglevel, tl, configdir):
|
||||||
else:
|
else:
|
||||||
brokermods = [get_brokermod(broker) for broker in brokers]
|
brokermods = [get_brokermod(broker) for broker in brokers]
|
||||||
|
|
||||||
|
reg_addr: None | tuple[str, int] = None
|
||||||
|
if host or port:
|
||||||
|
reg_addr = (
|
||||||
|
host or _registry_host,
|
||||||
|
int(port) or _registry_port,
|
||||||
|
)
|
||||||
|
|
||||||
ctx.obj.update({
|
ctx.obj.update({
|
||||||
'brokers': brokers,
|
'brokers': brokers,
|
||||||
'brokermods': brokermods,
|
'brokermods': brokermods,
|
||||||
|
@ -125,6 +164,7 @@ def cli(ctx, brokers, loglevel, tl, configdir):
|
||||||
'log': get_console_log(loglevel),
|
'log': get_console_log(loglevel),
|
||||||
'confdir': config._config_dir,
|
'confdir': config._config_dir,
|
||||||
'wl_path': config._watchlists_data_path,
|
'wl_path': config._watchlists_data_path,
|
||||||
|
'registry_addr': reg_addr,
|
||||||
})
|
})
|
||||||
|
|
||||||
# allow enabling same loglevel in ``tractor`` machinery
|
# allow enabling same loglevel in ``tractor`` machinery
|
||||||
|
|
|
@ -746,12 +746,12 @@ async def manage_history(
|
||||||
|
|
||||||
# we expect the sub-actor to write
|
# we expect the sub-actor to write
|
||||||
readonly=False,
|
readonly=False,
|
||||||
size=4*_secs_in_day,
|
size=3*_secs_in_day,
|
||||||
)
|
)
|
||||||
|
|
||||||
# (for now) set the rt (hft) shm array with space to prepend
|
# (for now) set the rt (hft) shm array with space to prepend
|
||||||
# only a few days worth of 1s history.
|
# only a few days worth of 1s history.
|
||||||
days = 3
|
days = 2
|
||||||
start_index = days*_secs_in_day
|
start_index = days*_secs_in_day
|
||||||
rt_shm._first.value = start_index
|
rt_shm._first.value = start_index
|
||||||
rt_shm._last.value = start_index
|
rt_shm._last.value = start_index
|
||||||
|
@ -1410,7 +1410,7 @@ async def open_feed(
|
||||||
|
|
||||||
# symbol.broker_info[brokername] = si
|
# symbol.broker_info[brokername] = si
|
||||||
feed.symbols[fqsn] = symbol
|
feed.symbols[fqsn] = symbol
|
||||||
feed.symbols[sym] = symbol
|
feed.symbols[f'{sym}.{brokername}'] = symbol
|
||||||
|
|
||||||
# cast shm dtype to list... can't member why we need this
|
# cast shm dtype to list... can't member why we need this
|
||||||
for shm_key, shm in [
|
for shm_key, shm in [
|
||||||
|
|
|
@ -66,7 +66,7 @@ async def _async_main(
|
||||||
# implicit required argument provided by ``qtractor_run()``
|
# implicit required argument provided by ``qtractor_run()``
|
||||||
main_widget: GodWidget,
|
main_widget: GodWidget,
|
||||||
|
|
||||||
sym: str,
|
syms: list[str],
|
||||||
brokernames: str,
|
brokernames: str,
|
||||||
loglevel: str,
|
loglevel: str,
|
||||||
|
|
||||||
|
@ -113,12 +113,16 @@ async def _async_main(
|
||||||
# godwidget.hbox.addWidget(search)
|
# godwidget.hbox.addWidget(search)
|
||||||
godwidget.search = search
|
godwidget.search = search
|
||||||
|
|
||||||
|
symbols: list[str] = []
|
||||||
|
|
||||||
|
for sym in syms:
|
||||||
symbol, _, provider = sym.rpartition('.')
|
symbol, _, provider = sym.rpartition('.')
|
||||||
|
symbols.append(symbol)
|
||||||
|
|
||||||
# this internally starts a ``display_symbol_data()`` task above
|
# this internally starts a ``display_symbol_data()`` task above
|
||||||
order_mode_ready = await godwidget.load_symbol(
|
order_mode_ready = await godwidget.load_symbols(
|
||||||
provider,
|
provider,
|
||||||
symbol,
|
symbols,
|
||||||
loglevel
|
loglevel
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -166,7 +170,7 @@ async def _async_main(
|
||||||
|
|
||||||
|
|
||||||
def _main(
|
def _main(
|
||||||
sym: str,
|
syms: list[str],
|
||||||
brokernames: [str],
|
brokernames: [str],
|
||||||
piker_loglevel: str,
|
piker_loglevel: str,
|
||||||
tractor_kwargs,
|
tractor_kwargs,
|
||||||
|
@ -178,7 +182,7 @@ def _main(
|
||||||
'''
|
'''
|
||||||
run_qtractor(
|
run_qtractor(
|
||||||
func=_async_main,
|
func=_async_main,
|
||||||
args=(sym, brokernames, piker_loglevel),
|
args=(syms, brokernames, piker_loglevel),
|
||||||
main_widget_type=GodWidget,
|
main_widget_type=GodWidget,
|
||||||
tractor_kwargs=tractor_kwargs,
|
tractor_kwargs=tractor_kwargs,
|
||||||
)
|
)
|
||||||
|
|
|
@ -186,10 +186,10 @@ class GodWidget(QWidget):
|
||||||
) -> tuple[LinkedSplits, LinkedSplits]: # type: ignore
|
) -> tuple[LinkedSplits, LinkedSplits]: # type: ignore
|
||||||
return self._chart_cache.get(symbol_key)
|
return self._chart_cache.get(symbol_key)
|
||||||
|
|
||||||
async def load_symbol(
|
async def load_symbols(
|
||||||
self,
|
self,
|
||||||
providername: str,
|
providername: str,
|
||||||
symbol_key: str,
|
symbol_keys: list[str],
|
||||||
loglevel: str,
|
loglevel: str,
|
||||||
reset: bool = False,
|
reset: bool = False,
|
||||||
|
|
||||||
|
@ -200,12 +200,20 @@ class GodWidget(QWidget):
|
||||||
Expects a ``numpy`` structured array containing all the ohlcv fields.
|
Expects a ``numpy`` structured array containing all the ohlcv fields.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
fqsns: list[str] = []
|
||||||
|
|
||||||
# our symbol key style is always lower case
|
# our symbol key style is always lower case
|
||||||
symbol_key = symbol_key.lower()
|
for key in list(map(str.lower, symbol_keys)):
|
||||||
|
|
||||||
# fully qualified symbol name (SNS i guess is what we're making?)
|
# fully qualified symbol name (SNS i guess is what we're making?)
|
||||||
fqsn = '.'.join([symbol_key, providername])
|
fqsn = '.'.join([key, providername])
|
||||||
all_linked = self.get_chart_symbol(fqsn)
|
fqsns.append(fqsn)
|
||||||
|
|
||||||
|
# NOTE: for now we use the first symbol in the set as the "key"
|
||||||
|
# for the overlay of feeds on the chart.
|
||||||
|
group_key = fqsns[0]
|
||||||
|
|
||||||
|
all_linked = self.get_chart_symbol(group_key)
|
||||||
order_mode_started = trio.Event()
|
order_mode_started = trio.Event()
|
||||||
|
|
||||||
if not self.vbox.isEmpty():
|
if not self.vbox.isEmpty():
|
||||||
|
@ -238,7 +246,7 @@ class GodWidget(QWidget):
|
||||||
display_symbol_data,
|
display_symbol_data,
|
||||||
self,
|
self,
|
||||||
providername,
|
providername,
|
||||||
symbol_key,
|
fqsns,
|
||||||
loglevel,
|
loglevel,
|
||||||
order_mode_started,
|
order_mode_started,
|
||||||
)
|
)
|
||||||
|
@ -814,7 +822,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# a better one?
|
# a better one?
|
||||||
def mk_vb(self, name: str) -> ChartView:
|
def mk_vb(self, name: str) -> ChartView:
|
||||||
cv = ChartView(name)
|
cv = ChartView(name)
|
||||||
cv.linkedsplits = self.linked
|
# link new view to chart's view set
|
||||||
|
cv.linked = self.linked
|
||||||
return cv
|
return cv
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -1179,19 +1188,27 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
)
|
)
|
||||||
pi.hideButtons()
|
pi.hideButtons()
|
||||||
|
|
||||||
# cv.enable_auto_yrange(self.view)
|
|
||||||
cv.enable_auto_yrange()
|
|
||||||
|
|
||||||
# compose this new plot's graphics with the current chart's
|
# compose this new plot's graphics with the current chart's
|
||||||
# existing one but with separate axes as neede and specified.
|
# existing one but with separate axes as neede and specified.
|
||||||
self.pi_overlay.add_plotitem(
|
self.pi_overlay.add_plotitem(
|
||||||
pi,
|
pi,
|
||||||
index=index,
|
index=index,
|
||||||
|
|
||||||
# only link x-axes,
|
# only link x-axes and
|
||||||
|
# don't relay any ``ViewBox`` derived event
|
||||||
|
# handlers since we only care about keeping charts
|
||||||
|
# x-synced on interaction (at least for now).
|
||||||
link_axes=(0,),
|
link_axes=(0,),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# connect auto-yrange callbacks *from* this new
|
||||||
|
# view **to** this parent and likewise *from* the
|
||||||
|
# main/parent chart back *to* the created overlay.
|
||||||
|
cv.enable_auto_yrange(src_vb=self.view)
|
||||||
|
# makes it so that interaction on the new overlay will reflect
|
||||||
|
# back on the main chart (which overlay was added to).
|
||||||
|
self.view.enable_auto_yrange(src_vb=cv)
|
||||||
|
|
||||||
# add axis title
|
# add axis title
|
||||||
# TODO: do we want this API to still work?
|
# TODO: do we want this API to still work?
|
||||||
# raxis = pi.getAxis('right')
|
# raxis = pi.getAxis('right')
|
||||||
|
|
|
@ -947,7 +947,7 @@ async def link_views_with_region(
|
||||||
async def display_symbol_data(
|
async def display_symbol_data(
|
||||||
godwidget: GodWidget,
|
godwidget: GodWidget,
|
||||||
provider: str,
|
provider: str,
|
||||||
sym: str,
|
fqsns: list[str],
|
||||||
loglevel: str,
|
loglevel: str,
|
||||||
order_mode_started: trio.Event,
|
order_mode_started: trio.Event,
|
||||||
|
|
||||||
|
@ -961,11 +961,6 @@ async def display_symbol_data(
|
||||||
|
|
||||||
'''
|
'''
|
||||||
sbar = godwidget.window.status_bar
|
sbar = godwidget.window.status_bar
|
||||||
loading_sym_key = sbar.open_status(
|
|
||||||
f'loading {sym}.{provider} ->',
|
|
||||||
group_key=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# historical data fetch
|
# historical data fetch
|
||||||
# brokermod = brokers.get_brokermod(provider)
|
# brokermod = brokers.get_brokermod(provider)
|
||||||
|
|
||||||
|
@ -974,10 +969,18 @@ async def display_symbol_data(
|
||||||
# clear_on_next=True,
|
# clear_on_next=True,
|
||||||
# group_key=loading_sym_key,
|
# group_key=loading_sym_key,
|
||||||
# )
|
# )
|
||||||
fqsn = '.'.join((sym, provider))
|
|
||||||
|
for fqsn in fqsns:
|
||||||
|
|
||||||
|
loading_sym_key = sbar.open_status(
|
||||||
|
f'loading {fqsn} ->',
|
||||||
|
group_key=True
|
||||||
|
)
|
||||||
|
|
||||||
|
first_fqsn = fqsns[0]
|
||||||
|
|
||||||
async with open_feed(
|
async with open_feed(
|
||||||
[fqsn],
|
fqsns,
|
||||||
loglevel=loglevel,
|
loglevel=loglevel,
|
||||||
|
|
||||||
# limit to at least display's FPS
|
# limit to at least display's FPS
|
||||||
|
@ -988,7 +991,7 @@ async def display_symbol_data(
|
||||||
ohlcv: ShmArray = feed.rt_shm
|
ohlcv: ShmArray = feed.rt_shm
|
||||||
hist_ohlcv: ShmArray = feed.hist_shm
|
hist_ohlcv: ShmArray = feed.hist_shm
|
||||||
|
|
||||||
symbol = feed.symbols[sym]
|
symbol = feed.symbols[first_fqsn]
|
||||||
fqsn = symbol.front_fqsn()
|
fqsn = symbol.front_fqsn()
|
||||||
|
|
||||||
step_size_s = 1
|
step_size_s = 1
|
||||||
|
@ -1025,7 +1028,7 @@ async def display_symbol_data(
|
||||||
godwidget.pp_pane = pp_pane
|
godwidget.pp_pane = pp_pane
|
||||||
|
|
||||||
# create main OHLC chart
|
# create main OHLC chart
|
||||||
chart = rt_linked.plot_ohlc_main(
|
ohlc_chart = rt_linked.plot_ohlc_main(
|
||||||
symbol,
|
symbol,
|
||||||
ohlcv,
|
ohlcv,
|
||||||
# in the case of history chart we explicitly set `False`
|
# in the case of history chart we explicitly set `False`
|
||||||
|
@ -1033,8 +1036,8 @@ async def display_symbol_data(
|
||||||
sidepane=pp_pane,
|
sidepane=pp_pane,
|
||||||
)
|
)
|
||||||
|
|
||||||
chart._feeds[symbol.key] = feed
|
ohlc_chart._feeds[symbol.key] = feed
|
||||||
chart.setFocus()
|
ohlc_chart.setFocus()
|
||||||
|
|
||||||
# XXX: FOR SOME REASON THIS IS CAUSING HANGZ!?!
|
# XXX: FOR SOME REASON THIS IS CAUSING HANGZ!?!
|
||||||
# plot historical vwap if available
|
# plot historical vwap if available
|
||||||
|
@ -1044,7 +1047,7 @@ async def display_symbol_data(
|
||||||
# and 'bar_wap' in bars.dtype.fields
|
# and 'bar_wap' in bars.dtype.fields
|
||||||
# ):
|
# ):
|
||||||
# wap_in_history = True
|
# wap_in_history = True
|
||||||
# chart.draw_curve(
|
# ohlc_chart.draw_curve(
|
||||||
# name='bar_wap',
|
# name='bar_wap',
|
||||||
# shm=ohlcv,
|
# shm=ohlcv,
|
||||||
# color='default_light',
|
# color='default_light',
|
||||||
|
@ -1105,7 +1108,7 @@ async def display_symbol_data(
|
||||||
await trio.sleep(0)
|
await trio.sleep(0)
|
||||||
|
|
||||||
# size view to data prior to order mode init
|
# size view to data prior to order mode init
|
||||||
chart.default_view()
|
ohlc_chart.default_view()
|
||||||
rt_linked.graphics_cycle()
|
rt_linked.graphics_cycle()
|
||||||
await trio.sleep(0)
|
await trio.sleep(0)
|
||||||
|
|
||||||
|
@ -1119,7 +1122,7 @@ async def display_symbol_data(
|
||||||
godwidget.resize_all()
|
godwidget.resize_all()
|
||||||
|
|
||||||
await link_views_with_region(
|
await link_views_with_region(
|
||||||
chart,
|
ohlc_chart,
|
||||||
hist_chart,
|
hist_chart,
|
||||||
feed,
|
feed,
|
||||||
)
|
)
|
||||||
|
@ -1135,7 +1138,7 @@ async def display_symbol_data(
|
||||||
):
|
):
|
||||||
if not vlm_chart:
|
if not vlm_chart:
|
||||||
# trigger another view reset if no sub-chart
|
# trigger another view reset if no sub-chart
|
||||||
chart.default_view()
|
ohlc_chart.default_view()
|
||||||
|
|
||||||
rt_linked.mode = mode
|
rt_linked.mode = mode
|
||||||
|
|
||||||
|
|
|
@ -624,6 +624,8 @@ async def open_vlm_displays(
|
||||||
# built-in vlm which we plot ASAP since it's
|
# built-in vlm which we plot ASAP since it's
|
||||||
# usually data provided directly with OHLC history.
|
# usually data provided directly with OHLC history.
|
||||||
shm = ohlcv
|
shm = ohlcv
|
||||||
|
ohlc_chart = linked.chart
|
||||||
|
|
||||||
chart = linked.add_plot(
|
chart = linked.add_plot(
|
||||||
name='volume',
|
name='volume',
|
||||||
shm=shm,
|
shm=shm,
|
||||||
|
@ -639,6 +641,9 @@ async def open_vlm_displays(
|
||||||
# the curve item internals are pretty convoluted.
|
# the curve item internals are pretty convoluted.
|
||||||
style='step',
|
style='step',
|
||||||
)
|
)
|
||||||
|
ohlc_chart.view.enable_auto_yrange(
|
||||||
|
src_vb=chart.view,
|
||||||
|
)
|
||||||
|
|
||||||
# force 0 to always be in view
|
# force 0 to always be in view
|
||||||
def multi_maxmin(
|
def multi_maxmin(
|
||||||
|
|
|
@ -329,7 +329,6 @@ async def handle_viewmode_mouse(
|
||||||
):
|
):
|
||||||
# when in order mode, submit execution
|
# when in order mode, submit execution
|
||||||
# msg.event.accept()
|
# msg.event.accept()
|
||||||
# breakpoint()
|
|
||||||
view.order_mode.submit_order()
|
view.order_mode.submit_order()
|
||||||
|
|
||||||
|
|
||||||
|
@ -346,16 +345,6 @@ class ChartView(ViewBox):
|
||||||
'''
|
'''
|
||||||
mode_name: str = 'view'
|
mode_name: str = 'view'
|
||||||
|
|
||||||
# "relay events" for making overlaid views work.
|
|
||||||
# NOTE: these MUST be defined here (and can't be monkey patched
|
|
||||||
# on later) due to signal construction requiring refs to be
|
|
||||||
# in place during the run of meta-class machinery.
|
|
||||||
mouseDragEventRelay = QtCore.Signal(object, object, object)
|
|
||||||
wheelEventRelay = QtCore.Signal(object, object, object)
|
|
||||||
|
|
||||||
event_relay_source: 'Optional[ViewBox]' = None
|
|
||||||
relays: dict[str, QtCore.Signal] = {}
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|
||||||
|
@ -479,7 +468,7 @@ class ChartView(ViewBox):
|
||||||
self,
|
self,
|
||||||
ev,
|
ev,
|
||||||
axis=None,
|
axis=None,
|
||||||
relayed_from: ChartView = None,
|
# relayed_from: ChartView = None,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Override "center-point" location for scrolling.
|
Override "center-point" location for scrolling.
|
||||||
|
@ -490,6 +479,13 @@ class ChartView(ViewBox):
|
||||||
TODO: PR a method into ``pyqtgraph`` to make this configurable
|
TODO: PR a method into ``pyqtgraph`` to make this configurable
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
linked = self.linked
|
||||||
|
if (
|
||||||
|
not linked
|
||||||
|
):
|
||||||
|
# print(f'{self.name} not linked but relay from {relayed_from.name}')
|
||||||
|
return
|
||||||
|
|
||||||
if axis in (0, 1):
|
if axis in (0, 1):
|
||||||
mask = [False, False]
|
mask = [False, False]
|
||||||
mask[axis] = self.state['mouseEnabled'][axis]
|
mask[axis] = self.state['mouseEnabled'][axis]
|
||||||
|
@ -609,9 +605,20 @@ class ChartView(ViewBox):
|
||||||
self,
|
self,
|
||||||
ev,
|
ev,
|
||||||
axis: Optional[int] = None,
|
axis: Optional[int] = None,
|
||||||
relayed_from: ChartView = None,
|
# relayed_from: ChartView = None,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
# if relayed_from:
|
||||||
|
# print(f'PAN: {self.name} -> RELAYED FROM: {relayed_from.name}')
|
||||||
|
|
||||||
|
# NOTE since in the overlay case axes are already
|
||||||
|
# "linked" any x-range change will already be mirrored
|
||||||
|
# in all overlaid ``PlotItems``, so we need to simply
|
||||||
|
# ignore the signal here since otherwise we get N-calls
|
||||||
|
# from N-overlays resulting in an "accelerated" feeling
|
||||||
|
# panning motion instead of the expect linear shift.
|
||||||
|
# if relayed_from:
|
||||||
|
# return
|
||||||
|
|
||||||
pos = ev.pos()
|
pos = ev.pos()
|
||||||
lastPos = ev.lastPos()
|
lastPos = ev.lastPos()
|
||||||
|
@ -849,29 +856,33 @@ class ChartView(ViewBox):
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
Assign callback for rescaling y-axis automatically
|
Assign callbacks for rescaling and resampling y-axis data
|
||||||
based on data contents and ``ViewBox`` state.
|
automatically based on data contents and ``ViewBox`` state.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
if src_vb is None:
|
if src_vb is None:
|
||||||
src_vb = self
|
src_vb = self
|
||||||
|
|
||||||
# splitter(s) resizing
|
# widget-UIs/splitter(s) resizing
|
||||||
src_vb.sigResized.connect(self._set_yrange)
|
src_vb.sigResized.connect(self._set_yrange)
|
||||||
|
|
||||||
|
# re-sampling trigger:
|
||||||
# TODO: a smarter way to avoid calling this needlessly?
|
# TODO: a smarter way to avoid calling this needlessly?
|
||||||
# 2 things i can think of:
|
# 2 things i can think of:
|
||||||
# - register downsample-able graphics specially and only
|
# - register downsample-able graphics specially and only
|
||||||
# iterate those.
|
# iterate those.
|
||||||
# - only register this when certain downsampleable graphics are
|
# - only register this when certain downsample-able graphics are
|
||||||
# "added to scene".
|
# "added to scene".
|
||||||
src_vb.sigRangeChangedManually.connect(
|
src_vb.sigRangeChangedManually.connect(
|
||||||
self.maybe_downsample_graphics
|
self.maybe_downsample_graphics
|
||||||
)
|
)
|
||||||
|
|
||||||
# mouse wheel doesn't emit XRangeChanged
|
# mouse wheel doesn't emit XRangeChanged
|
||||||
src_vb.sigRangeChangedManually.connect(self._set_yrange)
|
src_vb.sigRangeChangedManually.connect(self._set_yrange)
|
||||||
|
|
||||||
|
# XXX: enabling these will cause "jittery"-ness
|
||||||
|
# on zoom where sharp diffs in the y-range will
|
||||||
|
# not re-size right away until a new sample update?
|
||||||
|
# if src_vb is not self:
|
||||||
# src_vb.sigXRangeChanged.connect(self._set_yrange)
|
# src_vb.sigXRangeChanged.connect(self._set_yrange)
|
||||||
# src_vb.sigXRangeChanged.connect(
|
# src_vb.sigXRangeChanged.connect(
|
||||||
# self.maybe_downsample_graphics
|
# self.maybe_downsample_graphics
|
||||||
|
@ -916,7 +927,6 @@ class ChartView(ViewBox):
|
||||||
self,
|
self,
|
||||||
autoscale_overlays: bool = True,
|
autoscale_overlays: bool = True,
|
||||||
):
|
):
|
||||||
|
|
||||||
profiler = Profiler(
|
profiler = Profiler(
|
||||||
msg=f'ChartView.maybe_downsample_graphics() for {self.name}',
|
msg=f'ChartView.maybe_downsample_graphics() for {self.name}',
|
||||||
disabled=not pg_profile_enabled(),
|
disabled=not pg_profile_enabled(),
|
||||||
|
@ -931,8 +941,12 @@ class ChartView(ViewBox):
|
||||||
|
|
||||||
# TODO: a faster single-loop-iterator way of doing this XD
|
# TODO: a faster single-loop-iterator way of doing this XD
|
||||||
chart = self._chart
|
chart = self._chart
|
||||||
|
plots = {chart.name: chart}
|
||||||
|
|
||||||
linked = self.linked
|
linked = self.linked
|
||||||
plots = linked.subplots | {chart.name: chart}
|
if linked:
|
||||||
|
plots |= linked.subplots
|
||||||
|
|
||||||
for chart_name, chart in plots.items():
|
for chart_name, chart in plots.items():
|
||||||
for name, flow in chart._flows.items():
|
for name, flow in chart._flows.items():
|
||||||
|
|
||||||
|
|
|
@ -18,23 +18,27 @@
|
||||||
Charting overlay helpers.
|
Charting overlay helpers.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from typing import Callable, Optional
|
from collections import defaultdict
|
||||||
|
from functools import partial
|
||||||
from pyqtgraph.Qt.QtCore import (
|
from typing import (
|
||||||
# QObject,
|
Callable,
|
||||||
# Signal,
|
Optional,
|
||||||
Qt,
|
|
||||||
# QEvent,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
from pyqtgraph.graphicsItems.AxisItem import AxisItem
|
from pyqtgraph.graphicsItems.AxisItem import AxisItem
|
||||||
from pyqtgraph.graphicsItems.ViewBox import ViewBox
|
from pyqtgraph.graphicsItems.ViewBox import ViewBox
|
||||||
from pyqtgraph.graphicsItems.GraphicsWidget import GraphicsWidget
|
# from pyqtgraph.graphicsItems.GraphicsWidget import GraphicsWidget
|
||||||
from pyqtgraph.graphicsItems.PlotItem.PlotItem import PlotItem
|
from pyqtgraph.graphicsItems.PlotItem.PlotItem import PlotItem
|
||||||
from pyqtgraph.Qt.QtCore import QObject, Signal, QEvent
|
from pyqtgraph.Qt.QtCore import (
|
||||||
from pyqtgraph.Qt.QtWidgets import QGraphicsGridLayout, QGraphicsLinearLayout
|
QObject,
|
||||||
|
Signal,
|
||||||
from ._interaction import ChartView
|
QEvent,
|
||||||
|
Qt,
|
||||||
|
)
|
||||||
|
from pyqtgraph.Qt.QtWidgets import (
|
||||||
|
# QGraphicsGridLayout,
|
||||||
|
QGraphicsLinearLayout,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = ["PlotItemOverlay"]
|
__all__ = ["PlotItemOverlay"]
|
||||||
|
|
||||||
|
@ -89,16 +93,11 @@ class ComposedGridLayout:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
item: PlotItem,
|
item: PlotItem,
|
||||||
grid: QGraphicsGridLayout,
|
|
||||||
reverse: bool = False, # insert items to the "center"
|
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
self.items: list[PlotItem] = []
|
|
||||||
# self.grid = grid
|
|
||||||
self.reverse = reverse
|
|
||||||
|
|
||||||
# TODO: use a ``bidict`` here?
|
self.items: list[PlotItem] = []
|
||||||
self._pi2axes: dict[
|
self._pi2axes: dict[ # TODO: use a ``bidict`` here?
|
||||||
int,
|
int,
|
||||||
dict[str, AxisItem],
|
dict[str, AxisItem],
|
||||||
] = {}
|
] = {}
|
||||||
|
@ -120,12 +119,13 @@ class ComposedGridLayout:
|
||||||
|
|
||||||
if name in ('top', 'bottom'):
|
if name in ('top', 'bottom'):
|
||||||
orient = Qt.Vertical
|
orient = Qt.Vertical
|
||||||
|
|
||||||
elif name in ('left', 'right'):
|
elif name in ('left', 'right'):
|
||||||
orient = Qt.Horizontal
|
orient = Qt.Horizontal
|
||||||
|
|
||||||
layout.setOrientation(orient)
|
layout.setOrientation(orient)
|
||||||
|
|
||||||
self.insert(0, item)
|
self.insert_plotitem(0, item)
|
||||||
|
|
||||||
# insert surrounding linear layouts into the parent pi's layout
|
# insert surrounding linear layouts into the parent pi's layout
|
||||||
# such that additional axes can be appended arbitrarily without
|
# such that additional axes can be appended arbitrarily without
|
||||||
|
@ -159,7 +159,7 @@ class ComposedGridLayout:
|
||||||
# enter plot into list for index tracking
|
# enter plot into list for index tracking
|
||||||
self.items.insert(index, plotitem)
|
self.items.insert(index, plotitem)
|
||||||
|
|
||||||
def insert(
|
def insert_plotitem(
|
||||||
self,
|
self,
|
||||||
index: int,
|
index: int,
|
||||||
plotitem: PlotItem,
|
plotitem: PlotItem,
|
||||||
|
@ -171,7 +171,9 @@ class ComposedGridLayout:
|
||||||
|
|
||||||
'''
|
'''
|
||||||
if index < 0:
|
if index < 0:
|
||||||
raise ValueError('`insert()` only supports an index >= 0')
|
raise ValueError(
|
||||||
|
'`.insert_plotitem()` only supports an index >= 0'
|
||||||
|
)
|
||||||
|
|
||||||
# add plot's axes in sequence to the embedded linear layouts
|
# add plot's axes in sequence to the embedded linear layouts
|
||||||
# for each "side" thus avoiding graphics collisions.
|
# for each "side" thus avoiding graphics collisions.
|
||||||
|
@ -220,7 +222,7 @@ class ComposedGridLayout:
|
||||||
|
|
||||||
return index
|
return index
|
||||||
|
|
||||||
def append(
|
def append_plotitem(
|
||||||
self,
|
self,
|
||||||
item: PlotItem,
|
item: PlotItem,
|
||||||
|
|
||||||
|
@ -232,7 +234,7 @@ class ComposedGridLayout:
|
||||||
'''
|
'''
|
||||||
# for left and bottom axes we have to first remove
|
# for left and bottom axes we have to first remove
|
||||||
# items and re-insert to maintain a list-order.
|
# items and re-insert to maintain a list-order.
|
||||||
return self.insert(len(self.items), item)
|
return self.insert_plotitem(len(self.items), item)
|
||||||
|
|
||||||
def get_axis(
|
def get_axis(
|
||||||
self,
|
self,
|
||||||
|
@ -249,16 +251,16 @@ class ComposedGridLayout:
|
||||||
named = self._pi2axes[name]
|
named = self._pi2axes[name]
|
||||||
return named.get(index)
|
return named.get(index)
|
||||||
|
|
||||||
def pop(
|
# def pop(
|
||||||
self,
|
# self,
|
||||||
item: PlotItem,
|
# item: PlotItem,
|
||||||
|
|
||||||
) -> PlotItem:
|
# ) -> PlotItem:
|
||||||
'''
|
# '''
|
||||||
Remove item and restack all axes in list-order.
|
# Remove item and restack all axes in list-order.
|
||||||
|
|
||||||
'''
|
# '''
|
||||||
raise NotImplementedError
|
# raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
# Unimplemented features TODO:
|
# Unimplemented features TODO:
|
||||||
|
@ -279,194 +281,6 @@ class ComposedGridLayout:
|
||||||
# axis?
|
# axis?
|
||||||
|
|
||||||
|
|
||||||
# TODO: we might want to enabled some kind of manual flag to disable
|
|
||||||
# this method wrapping during type creation? As example a user could
|
|
||||||
# definitively decide **not** to enable broadcasting support by
|
|
||||||
# setting something like ``ViewBox.disable_relays = True``?
|
|
||||||
def mk_relay_method(
|
|
||||||
|
|
||||||
signame: str,
|
|
||||||
slot: Callable[
|
|
||||||
[ViewBox,
|
|
||||||
'QEvent',
|
|
||||||
Optional[AxisItem]],
|
|
||||||
None,
|
|
||||||
],
|
|
||||||
|
|
||||||
) -> Callable[
|
|
||||||
[
|
|
||||||
ViewBox,
|
|
||||||
# lol, there isn't really a generic type thanks
|
|
||||||
# to the rewrite of Qt's event system XD
|
|
||||||
'QEvent',
|
|
||||||
|
|
||||||
'Optional[AxisItem]',
|
|
||||||
'Optional[ViewBox]', # the ``relayed_from`` arg we provide
|
|
||||||
],
|
|
||||||
None,
|
|
||||||
]:
|
|
||||||
|
|
||||||
def maybe_broadcast(
|
|
||||||
vb: 'ViewBox',
|
|
||||||
ev: 'QEvent',
|
|
||||||
axis: 'Optional[int]' = None,
|
|
||||||
relayed_from: 'ViewBox' = None,
|
|
||||||
|
|
||||||
) -> None:
|
|
||||||
'''
|
|
||||||
(soon to be) Decorator which makes an event handler
|
|
||||||
"broadcastable" to overlayed ``GraphicsWidget``s.
|
|
||||||
|
|
||||||
Adds relay signals based on the decorated handler's name
|
|
||||||
and conducts a signal broadcast of the relay signal if there
|
|
||||||
are consumers registered.
|
|
||||||
|
|
||||||
'''
|
|
||||||
# When no relay source has been set just bypass all
|
|
||||||
# the broadcast machinery.
|
|
||||||
if vb.event_relay_source is None:
|
|
||||||
ev.accept()
|
|
||||||
return slot(
|
|
||||||
vb,
|
|
||||||
ev,
|
|
||||||
axis=axis,
|
|
||||||
)
|
|
||||||
|
|
||||||
if relayed_from:
|
|
||||||
assert axis is None
|
|
||||||
|
|
||||||
# this is a relayed event and should be ignored (so it does not
|
|
||||||
# halt/short circuit the graphicscene loop). Further the
|
|
||||||
# surrounding handler for this signal must be allowed to execute
|
|
||||||
# and get processed by **this consumer**.
|
|
||||||
# print(f'{vb.name} rx relayed from {relayed_from.name}')
|
|
||||||
ev.ignore()
|
|
||||||
|
|
||||||
return slot(
|
|
||||||
vb,
|
|
||||||
ev,
|
|
||||||
axis=axis,
|
|
||||||
)
|
|
||||||
|
|
||||||
if axis is not None:
|
|
||||||
# print(f'{vb.name} handling axis event:\n{str(ev)}')
|
|
||||||
ev.accept()
|
|
||||||
return slot(
|
|
||||||
vb,
|
|
||||||
ev,
|
|
||||||
axis=axis,
|
|
||||||
)
|
|
||||||
|
|
||||||
elif (
|
|
||||||
relayed_from is None
|
|
||||||
and vb.event_relay_source is vb # we are the broadcaster
|
|
||||||
and axis is None
|
|
||||||
):
|
|
||||||
# Broadcast case: this is a source event which will be
|
|
||||||
# relayed to attached consumers and accepted after all
|
|
||||||
# consumers complete their own handling followed by this
|
|
||||||
# routine's processing. Sequence is,
|
|
||||||
# - pre-relay to all consumers *first* - ``.emit()`` blocks
|
|
||||||
# until all downstream relay handlers have run.
|
|
||||||
# - run the source handler for **this** event and accept
|
|
||||||
# the event
|
|
||||||
|
|
||||||
# Access the "bound signal" that is created
|
|
||||||
# on the widget type as part of instantiation.
|
|
||||||
signal = getattr(vb, signame)
|
|
||||||
# print(f'{vb.name} emitting {signame}')
|
|
||||||
|
|
||||||
# TODO/NOTE: we could also just bypass a "relay" signal
|
|
||||||
# entirely and instead call the handlers manually in
|
|
||||||
# a loop? This probably is a lot simpler and also doesn't
|
|
||||||
# have any downside, and allows not touching target widget
|
|
||||||
# internals.
|
|
||||||
signal.emit(
|
|
||||||
ev,
|
|
||||||
axis,
|
|
||||||
# passing this demarks a broadcasted/relayed event
|
|
||||||
vb,
|
|
||||||
)
|
|
||||||
# accept event so no more relays are fired.
|
|
||||||
ev.accept()
|
|
||||||
|
|
||||||
# call underlying wrapped method with an extra
|
|
||||||
# ``relayed_from`` value to denote that this is a relayed
|
|
||||||
# event handling case.
|
|
||||||
return slot(
|
|
||||||
vb,
|
|
||||||
ev,
|
|
||||||
axis=axis,
|
|
||||||
)
|
|
||||||
|
|
||||||
return maybe_broadcast
|
|
||||||
|
|
||||||
|
|
||||||
# XXX: :( can't define signals **after** class compile time
|
|
||||||
# so this is not really useful.
|
|
||||||
# def mk_relay_signal(
|
|
||||||
# func,
|
|
||||||
# name: str = None,
|
|
||||||
|
|
||||||
# ) -> Signal:
|
|
||||||
# (
|
|
||||||
# args,
|
|
||||||
# varargs,
|
|
||||||
# varkw,
|
|
||||||
# defaults,
|
|
||||||
# kwonlyargs,
|
|
||||||
# kwonlydefaults,
|
|
||||||
# annotations
|
|
||||||
# ) = inspect.getfullargspec(func)
|
|
||||||
|
|
||||||
# # XXX: generate a relay signal with 1 extra
|
|
||||||
# # argument for a ``relayed_from`` kwarg. Since
|
|
||||||
# # ``'self'`` is already ignored by signals we just need
|
|
||||||
# # to count the arguments since we're adding only 1 (and
|
|
||||||
# # ``args`` will capture that).
|
|
||||||
# numargs = len(args + list(defaults))
|
|
||||||
# signal = Signal(*tuple(numargs * [object]))
|
|
||||||
# signame = name or func.__name__ + 'Relay'
|
|
||||||
# return signame, signal
|
|
||||||
|
|
||||||
|
|
||||||
def enable_relays(
|
|
||||||
widget: GraphicsWidget,
|
|
||||||
handler_names: list[str],
|
|
||||||
|
|
||||||
) -> list[Signal]:
|
|
||||||
'''
|
|
||||||
Method override helper which enables relay of a particular
|
|
||||||
``Signal`` from some chosen broadcaster widget to a set of
|
|
||||||
consumer widgets which should operate their event handlers normally
|
|
||||||
but instead of signals "relayed" from the broadcaster.
|
|
||||||
|
|
||||||
Mostly useful for overlaying widgets that handle user input
|
|
||||||
that you want to overlay graphically. The target ``widget`` type must
|
|
||||||
define ``QtCore.Signal``s each with a `'Relay'` suffix for each
|
|
||||||
name provided in ``handler_names: list[str]``.
|
|
||||||
|
|
||||||
'''
|
|
||||||
signals = []
|
|
||||||
for name in handler_names:
|
|
||||||
handler = getattr(widget, name)
|
|
||||||
signame = name + 'Relay'
|
|
||||||
# ensure the target widget defines a relay signal
|
|
||||||
relay = getattr(widget, signame)
|
|
||||||
widget.relays[signame] = name
|
|
||||||
signals.append(relay)
|
|
||||||
method = mk_relay_method(signame, handler)
|
|
||||||
setattr(widget, name, method)
|
|
||||||
|
|
||||||
return signals
|
|
||||||
|
|
||||||
|
|
||||||
enable_relays(
|
|
||||||
ChartView,
|
|
||||||
['wheelEvent', 'mouseDragEvent']
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PlotItemOverlay:
|
class PlotItemOverlay:
|
||||||
'''
|
'''
|
||||||
A composite for managing overlaid ``PlotItem`` instances such that
|
A composite for managing overlaid ``PlotItem`` instances such that
|
||||||
|
@ -482,16 +296,18 @@ class PlotItemOverlay:
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
self.root_plotitem: PlotItem = root_plotitem
|
self.root_plotitem: PlotItem = root_plotitem
|
||||||
|
self.relay_handlers: defaultdict[
|
||||||
|
str,
|
||||||
|
list[Callable],
|
||||||
|
] = defaultdict(list)
|
||||||
|
|
||||||
vb = root_plotitem.vb
|
# NOTE: required for scene layering/relaying; this guarantees
|
||||||
vb.event_relay_source = vb # TODO: maybe change name?
|
# the "root" plot receives priority for interaction
|
||||||
vb.setZValue(1000) # XXX: critical for scene layering/relaying
|
# events/signals.
|
||||||
|
root_plotitem.vb.setZValue(1000)
|
||||||
|
|
||||||
self.overlays: list[PlotItem] = []
|
self.overlays: list[PlotItem] = []
|
||||||
self.layout = ComposedGridLayout(
|
self.layout = ComposedGridLayout(root_plotitem)
|
||||||
root_plotitem,
|
|
||||||
root_plotitem.layout,
|
|
||||||
)
|
|
||||||
self._relays: dict[str, Signal] = {}
|
self._relays: dict[str, Signal] = {}
|
||||||
|
|
||||||
def add_plotitem(
|
def add_plotitem(
|
||||||
|
@ -499,8 +315,10 @@ class PlotItemOverlay:
|
||||||
plotitem: PlotItem,
|
plotitem: PlotItem,
|
||||||
index: Optional[int] = None,
|
index: Optional[int] = None,
|
||||||
|
|
||||||
# TODO: we could also put the ``ViewBox.XAxis``
|
# event/signal names which will be broadcasted to all added
|
||||||
# style enum here?
|
# (relayee) ``PlotItem``s (eg. ``ViewBox.mouseDragEvent``).
|
||||||
|
relay_events: list[str] = [],
|
||||||
|
|
||||||
# (0,), # link x
|
# (0,), # link x
|
||||||
# (1,), # link y
|
# (1,), # link y
|
||||||
# (0, 1), # link both
|
# (0, 1), # link both
|
||||||
|
@ -510,58 +328,155 @@ class PlotItemOverlay:
|
||||||
|
|
||||||
index = index or len(self.overlays)
|
index = index or len(self.overlays)
|
||||||
root = self.root_plotitem
|
root = self.root_plotitem
|
||||||
# layout: QGraphicsGridLayout = root.layout
|
|
||||||
self.overlays.insert(index, plotitem)
|
self.overlays.insert(index, plotitem)
|
||||||
vb: ViewBox = plotitem.vb
|
vb: ViewBox = plotitem.vb
|
||||||
|
|
||||||
# mark this consumer overlay as ready to expect relayed events
|
|
||||||
# from the root plotitem.
|
|
||||||
vb.event_relay_source = root.vb
|
|
||||||
|
|
||||||
# TODO: some sane way to allow menu event broadcast XD
|
# TODO: some sane way to allow menu event broadcast XD
|
||||||
# vb.setMenuEnabled(False)
|
# vb.setMenuEnabled(False)
|
||||||
|
|
||||||
# TODO: inside the `maybe_broadcast()` (soon to be) decorator
|
# wire up any relay signal(s) from the source plot to added
|
||||||
# we need have checks that consumers have been attached to
|
# "overlays". We use a plain loop instead of mucking with
|
||||||
# these relay signals.
|
# re-connecting signal/slots which tends to be more invasive and
|
||||||
if link_axes != (0, 1):
|
# harder to implement and provides no measurable performance
|
||||||
|
# gain.
|
||||||
|
if relay_events:
|
||||||
|
for ev_name in relay_events:
|
||||||
|
relayee_handler: Callable[
|
||||||
|
[
|
||||||
|
ViewBox,
|
||||||
|
# lol, there isn't really a generic type thanks
|
||||||
|
# to the rewrite of Qt's event system XD
|
||||||
|
QEvent,
|
||||||
|
|
||||||
# wire up relay signals
|
AxisItem | None,
|
||||||
for relay_signal_name, handler_name in vb.relays.items():
|
],
|
||||||
# print(handler_name)
|
None,
|
||||||
# XXX: Signal class attrs are bound after instantiation
|
] = getattr(vb, ev_name)
|
||||||
# of the defining type, so we need to access that bound
|
|
||||||
# version here.
|
sub_handlers: list[Callable] = self.relay_handlers[ev_name]
|
||||||
signal = getattr(root.vb, relay_signal_name)
|
|
||||||
handler = getattr(vb, handler_name)
|
# on the first registry of a relayed event we pop the
|
||||||
signal.connect(handler)
|
# root's handler and override it to a custom broadcaster
|
||||||
|
# routine.
|
||||||
|
if not sub_handlers:
|
||||||
|
|
||||||
|
src_handler = getattr(
|
||||||
|
root.vb,
|
||||||
|
ev_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
def broadcast(
|
||||||
|
ev: 'QEvent',
|
||||||
|
|
||||||
|
# TODO: drop this viewbox specific input and
|
||||||
|
# allow a predicate to be passed in by user.
|
||||||
|
axis: 'Optional[int]' = None,
|
||||||
|
|
||||||
|
*,
|
||||||
|
|
||||||
|
# these are bound in by the ``partial`` below
|
||||||
|
# and ensure a unique broadcaster per event.
|
||||||
|
ev_name: str = None,
|
||||||
|
src_handler: Callable = None,
|
||||||
|
relayed_from: 'ViewBox' = None,
|
||||||
|
|
||||||
|
# remaining inputs the source handler expects
|
||||||
|
**kwargs,
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Broadcast signal or event: this is a source
|
||||||
|
event which will be relayed to attached
|
||||||
|
"relayee" plot item consumers.
|
||||||
|
|
||||||
|
The event is accepted halting any further
|
||||||
|
handlers from being triggered.
|
||||||
|
|
||||||
|
Sequence is,
|
||||||
|
- pre-relay to all consumers *first* - exactly
|
||||||
|
like how a ``Signal.emit()`` blocks until all
|
||||||
|
downstream relay handlers have run.
|
||||||
|
- run the event's source handler event
|
||||||
|
|
||||||
|
'''
|
||||||
|
ev.accept()
|
||||||
|
|
||||||
|
# broadcast first to relayees *first*. trigger
|
||||||
|
# relay of event to all consumers **before**
|
||||||
|
# processing/consumption in the source handler.
|
||||||
|
relayed_handlers = self.relay_handlers[ev_name]
|
||||||
|
|
||||||
|
assert getattr(vb, ev_name).__name__ == ev_name
|
||||||
|
|
||||||
|
# TODO: generalize as an input predicate
|
||||||
|
if axis is None:
|
||||||
|
for handler in relayed_handlers:
|
||||||
|
handler(
|
||||||
|
ev,
|
||||||
|
axis=axis,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
# run "source" widget's handler last
|
||||||
|
src_handler(
|
||||||
|
ev,
|
||||||
|
axis=axis,
|
||||||
|
)
|
||||||
|
|
||||||
|
# dynamic handler override on the publisher plot
|
||||||
|
setattr(
|
||||||
|
root.vb,
|
||||||
|
ev_name,
|
||||||
|
partial(
|
||||||
|
broadcast,
|
||||||
|
ev_name=ev_name,
|
||||||
|
src_handler=src_handler
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
assert getattr(root.vb, ev_name)
|
||||||
|
assert relayee_handler not in sub_handlers
|
||||||
|
|
||||||
|
# append relayed-to widget's handler to relay table
|
||||||
|
sub_handlers.append(relayee_handler)
|
||||||
|
|
||||||
# link dim-axes to root if requested by user.
|
# link dim-axes to root if requested by user.
|
||||||
# TODO: solve more-then-wanted scaled panning on click drag
|
|
||||||
# which seems to be due to broadcast. So we probably need to
|
|
||||||
# disable broadcast when axes are linked in a particular
|
|
||||||
# dimension?
|
|
||||||
for dim in link_axes:
|
for dim in link_axes:
|
||||||
# link x and y axes to new view box such that the top level
|
# link x and y axes to new view box such that the top level
|
||||||
# viewbox propagates to the root (and whatever other
|
# viewbox propagates to the root (and whatever other
|
||||||
# plotitem overlays that have been added).
|
# plotitem overlays that have been added).
|
||||||
vb.linkView(dim, root.vb)
|
vb.linkView(dim, root.vb)
|
||||||
|
|
||||||
# make overlaid viewbox impossible to focus since the top
|
# => NOTE: in order to prevent "more-then-linear" scaled
|
||||||
# level should handle all input and relay to overlays.
|
# panning moves on (for eg. click-drag) certain range change
|
||||||
# NOTE: this was solved with the `setZValue()` above!
|
# signals (i.e. ``.sigXRangeChanged``), the user needs to be
|
||||||
|
# careful that any broadcasted ``relay_events`` are are short
|
||||||
|
# circuited in sub-handlers (aka relayee's) implementations. As
|
||||||
|
# an example if a ``ViewBox.mouseDragEvent`` is broadcasted, the
|
||||||
|
# overlayed implementations need to be sure they either don't
|
||||||
|
# also link the x-axes (by not providing ``link_axes=(0,)``
|
||||||
|
# above) or that the relayee ``.mouseDragEvent()`` handlers are
|
||||||
|
# ready to "``return`` early" in the case that
|
||||||
|
# ``.sigXRangeChanged`` is emitted as part of linked axes.
|
||||||
|
# For more details on such signalling mechanics peek in
|
||||||
|
# ``ViewBox.linkView()``.
|
||||||
|
|
||||||
# TODO: we will probably want to add a "focus" api such that
|
# make overlaid viewbox impossible to focus since the top level
|
||||||
# a new "top level" ``PlotItem`` can be selected dynamically
|
# should handle all input and relay to overlays. Note that the
|
||||||
# (and presumably the axes dynamically sorted to match).
|
# "root" plot item gettingn interaction priority is configured
|
||||||
|
# with the ``.setZValue()`` during init.
|
||||||
vb.setFlag(
|
vb.setFlag(
|
||||||
vb.GraphicsItemFlag.ItemIsFocusable,
|
vb.GraphicsItemFlag.ItemIsFocusable,
|
||||||
False
|
False
|
||||||
)
|
)
|
||||||
vb.setFocusPolicy(Qt.NoFocus)
|
vb.setFocusPolicy(Qt.NoFocus)
|
||||||
|
|
||||||
|
# => TODO: add a "focus" api for switching the "top level"
|
||||||
|
# ``PlotItem`` dynamically.
|
||||||
|
|
||||||
# append-compose into the layout all axes from this plot
|
# append-compose into the layout all axes from this plot
|
||||||
self.layout.insert(index, plotitem)
|
self.layout.insert_plotitem(index, plotitem)
|
||||||
|
|
||||||
plotitem.setGeometry(root.vb.sceneBoundingRect())
|
plotitem.setGeometry(root.vb.sceneBoundingRect())
|
||||||
|
|
||||||
|
@ -579,25 +494,6 @@ class PlotItemOverlay:
|
||||||
root.vb.setFocus()
|
root.vb.setFocus()
|
||||||
assert root.vb.focusWidget()
|
assert root.vb.focusWidget()
|
||||||
|
|
||||||
# XXX: do we need this? Why would you build then destroy?
|
|
||||||
def remove_plotitem(self, plotItem: PlotItem) -> None:
|
|
||||||
'''
|
|
||||||
Remove this ``PlotItem`` from the overlayed set making not shown
|
|
||||||
and unable to accept input.
|
|
||||||
|
|
||||||
'''
|
|
||||||
...
|
|
||||||
|
|
||||||
# TODO: i think this would be super hot B)
|
|
||||||
def focus_item(self, plotitem: PlotItem) -> PlotItem:
|
|
||||||
'''
|
|
||||||
Apply focus to a contained PlotItem thus making it the "top level"
|
|
||||||
item in the overlay able to accept peripheral's input from the user
|
|
||||||
and responsible for zoom and panning control via its ``ViewBox``.
|
|
||||||
|
|
||||||
'''
|
|
||||||
...
|
|
||||||
|
|
||||||
def get_axis(
|
def get_axis(
|
||||||
self,
|
self,
|
||||||
plot: PlotItem,
|
plot: PlotItem,
|
||||||
|
@ -630,8 +526,9 @@ class PlotItemOverlay:
|
||||||
|
|
||||||
return axes
|
return axes
|
||||||
|
|
||||||
# TODO: i guess we need this if you want to detach existing plots
|
# XXX: untested as of now.
|
||||||
# dynamically? XXX: untested as of now.
|
# TODO: need this as part of selecting a different root/source
|
||||||
|
# plot to rewire interaction event broadcast dynamically.
|
||||||
def _disconnect_all(
|
def _disconnect_all(
|
||||||
self,
|
self,
|
||||||
plotitem: PlotItem,
|
plotitem: PlotItem,
|
||||||
|
@ -646,3 +543,22 @@ class PlotItemOverlay:
|
||||||
disconnected.append(sig)
|
disconnected.append(sig)
|
||||||
|
|
||||||
return disconnected
|
return disconnected
|
||||||
|
|
||||||
|
# XXX: do we need this? Why would you build then destroy?
|
||||||
|
# def remove_plotitem(self, plotItem: PlotItem) -> None:
|
||||||
|
# '''
|
||||||
|
# Remove this ``PlotItem`` from the overlayed set making not shown
|
||||||
|
# and unable to accept input.
|
||||||
|
|
||||||
|
# '''
|
||||||
|
# ...
|
||||||
|
|
||||||
|
# TODO: i think this would be super hot B)
|
||||||
|
# def focus_plotitem(self, plotitem: PlotItem) -> PlotItem:
|
||||||
|
# '''
|
||||||
|
# Apply focus to a contained PlotItem thus making it the "top level"
|
||||||
|
# item in the overlay able to accept peripheral's input from the user
|
||||||
|
# and responsible for zoom and panning control via its ``ViewBox``.
|
||||||
|
|
||||||
|
# '''
|
||||||
|
# ...
|
||||||
|
|
|
@ -665,9 +665,9 @@ class SearchWidget(QtWidgets.QWidget):
|
||||||
|
|
||||||
log.info(f'Requesting symbol: {symbol}.{provider}')
|
log.info(f'Requesting symbol: {symbol}.{provider}')
|
||||||
|
|
||||||
await godw.load_symbol(
|
await godw.load_symbols(
|
||||||
provider,
|
provider,
|
||||||
symbol,
|
[symbol],
|
||||||
'info',
|
'info',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -46,8 +46,10 @@ def _kivy_import_hack():
|
||||||
@click.argument('name', nargs=1, required=True)
|
@click.argument('name', nargs=1, required=True)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def monitor(config, rate, name, dhost, test, tl):
|
def monitor(config, rate, name, dhost, test, tl):
|
||||||
"""Start a real-time watchlist UI
|
'''
|
||||||
"""
|
Start a real-time watchlist UI
|
||||||
|
|
||||||
|
'''
|
||||||
# global opts
|
# global opts
|
||||||
brokermod = config['brokermods'][0]
|
brokermod = config['brokermods'][0]
|
||||||
loglevel = config['loglevel']
|
loglevel = config['loglevel']
|
||||||
|
@ -70,8 +72,12 @@ def monitor(config, rate, name, dhost, test, tl):
|
||||||
) as portal:
|
) as portal:
|
||||||
# run app "main"
|
# run app "main"
|
||||||
await _async_main(
|
await _async_main(
|
||||||
name, portal, tickers,
|
name,
|
||||||
brokermod, rate, test=test,
|
portal,
|
||||||
|
tickers,
|
||||||
|
brokermod,
|
||||||
|
rate,
|
||||||
|
test=test,
|
||||||
)
|
)
|
||||||
|
|
||||||
tractor.run(
|
tractor.run(
|
||||||
|
@ -122,7 +128,7 @@ def optschain(config, symbol, date, rate, test):
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@click.option(
|
@click.option(
|
||||||
'--profile',
|
'--profile',
|
||||||
'-p',
|
# '-p',
|
||||||
default=None,
|
default=None,
|
||||||
help='Enable pyqtgraph profiling'
|
help='Enable pyqtgraph profiling'
|
||||||
)
|
)
|
||||||
|
@ -131,9 +137,14 @@ def optschain(config, symbol, date, rate, test):
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
help='Enable tractor debug mode'
|
help='Enable tractor debug mode'
|
||||||
)
|
)
|
||||||
@click.argument('symbol', required=True)
|
@click.argument('symbols', nargs=-1, required=True)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def chart(config, symbol, profile, pdb):
|
def chart(
|
||||||
|
config,
|
||||||
|
symbols: list[str],
|
||||||
|
profile,
|
||||||
|
pdb: bool,
|
||||||
|
):
|
||||||
'''
|
'''
|
||||||
Start a real-time chartng UI
|
Start a real-time chartng UI
|
||||||
|
|
||||||
|
@ -144,8 +155,10 @@ def chart(config, symbol, profile, pdb):
|
||||||
_profile._pg_profile = True
|
_profile._pg_profile = True
|
||||||
_profile.ms_slower_then = float(profile)
|
_profile.ms_slower_then = float(profile)
|
||||||
|
|
||||||
|
# Qt UI entrypoint
|
||||||
from ._app import _main
|
from ._app import _main
|
||||||
|
|
||||||
|
for symbol in symbols:
|
||||||
if '.' not in symbol:
|
if '.' not in symbol:
|
||||||
click.echo(click.style(
|
click.echo(click.style(
|
||||||
f'symbol: {symbol} must have a {symbol}.<provider> suffix',
|
f'symbol: {symbol} must have a {symbol}.<provider> suffix',
|
||||||
|
@ -159,8 +172,9 @@ def chart(config, symbol, profile, pdb):
|
||||||
tractorloglevel = config['tractorloglevel']
|
tractorloglevel = config['tractorloglevel']
|
||||||
pikerloglevel = config['loglevel']
|
pikerloglevel = config['loglevel']
|
||||||
|
|
||||||
|
|
||||||
_main(
|
_main(
|
||||||
sym=symbol,
|
syms=symbols,
|
||||||
brokernames=brokernames,
|
brokernames=brokernames,
|
||||||
piker_loglevel=pikerloglevel,
|
piker_loglevel=pikerloglevel,
|
||||||
tractor_kwargs={
|
tractor_kwargs={
|
||||||
|
@ -170,5 +184,6 @@ def chart(config, symbol, profile, pdb):
|
||||||
'enable_modules': [
|
'enable_modules': [
|
||||||
'piker.clearing._client'
|
'piker.clearing._client'
|
||||||
],
|
],
|
||||||
|
'registry_addr': config.get('registry_addr'),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,15 +14,6 @@ def pytest_addoption(parser):
|
||||||
help="Use a practice API account")
|
help="Use a practice API account")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session', autouse=True)
|
|
||||||
def loglevel(request):
|
|
||||||
orig = tractor.log._default_loglevel
|
|
||||||
level = tractor.log._default_loglevel = request.config.option.loglevel
|
|
||||||
log.get_console_log(level)
|
|
||||||
yield level
|
|
||||||
tractor.log._default_loglevel = orig
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture(scope='session')
|
||||||
def test_config():
|
def test_config():
|
||||||
dirname = os.path.dirname
|
dirname = os.path.dirname
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
'''
|
||||||
|
Data feed layer APIs, performance, msg throttling.
|
||||||
|
|
||||||
|
'''
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import trio
|
||||||
|
from piker import (
|
||||||
|
open_piker_runtime,
|
||||||
|
open_feed,
|
||||||
|
)
|
||||||
|
from piker.data import ShmArray
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'fqsns',
|
||||||
|
[
|
||||||
|
['btcusdt.binance']
|
||||||
|
],
|
||||||
|
ids=lambda param: f'fqsns={param}',
|
||||||
|
)
|
||||||
|
def test_basic_rt_feed(
|
||||||
|
fqsns: list[str],
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Start a real-time data feed for provided fqsn and pull
|
||||||
|
a few quotes then simply shut down.
|
||||||
|
|
||||||
|
'''
|
||||||
|
async def main():
|
||||||
|
async with (
|
||||||
|
open_piker_runtime('test_basic_rt_feed'),
|
||||||
|
open_feed(
|
||||||
|
fqsns,
|
||||||
|
loglevel='info',
|
||||||
|
|
||||||
|
# TODO: ensure throttle rate is applied
|
||||||
|
# limit to at least display's FPS
|
||||||
|
# avoiding needless Qt-in-guest-mode context switches
|
||||||
|
# tick_throttle=_quote_throttle_rate,
|
||||||
|
|
||||||
|
) as feed
|
||||||
|
):
|
||||||
|
for fqin in fqsns:
|
||||||
|
assert feed.symbols[fqin]
|
||||||
|
|
||||||
|
ohlcv: ShmArray = feed.rt_shm
|
||||||
|
hist_ohlcv: ShmArray = feed.hist_shm
|
||||||
|
|
||||||
|
count: int = 0
|
||||||
|
async for quotes in feed.stream:
|
||||||
|
|
||||||
|
# print quote msg, rt and history
|
||||||
|
# buffer values on console.
|
||||||
|
pprint(quotes)
|
||||||
|
pprint(ohlcv.array[-1])
|
||||||
|
pprint(hist_ohlcv.array[-1])
|
||||||
|
|
||||||
|
if count >= 100:
|
||||||
|
break
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
trio.run(main)
|
Loading…
Reference in New Issue