commit
d6c9834a9a
|
@ -470,10 +470,14 @@ class Client:
|
||||||
|
|
||||||
# TODO add search though our adhoc-locally defined symbol set
|
# TODO add search though our adhoc-locally defined symbol set
|
||||||
# for futes/cmdtys/
|
# for futes/cmdtys/
|
||||||
|
try:
|
||||||
results = await self.search_stocks(
|
results = await self.search_stocks(
|
||||||
pattern,
|
pattern,
|
||||||
upto=upto,
|
upto=upto,
|
||||||
)
|
)
|
||||||
|
except ConnectionError:
|
||||||
|
return {}
|
||||||
|
|
||||||
for key, deats in results.copy().items():
|
for key, deats in results.copy().items():
|
||||||
|
|
||||||
tract = deats.contract
|
tract = deats.contract
|
||||||
|
|
|
@ -93,6 +93,9 @@ class Allocator(Struct):
|
||||||
else:
|
else:
|
||||||
return self.units_limit
|
return self.units_limit
|
||||||
|
|
||||||
|
def limit_info(self) -> tuple[str, float]:
|
||||||
|
return self.size_unit, self.limit()
|
||||||
|
|
||||||
def next_order_info(
|
def next_order_info(
|
||||||
self,
|
self,
|
||||||
|
|
||||||
|
|
|
@ -617,8 +617,9 @@ async def translate_and_relay_brokerd_events(
|
||||||
f'Received broker trade event:\n'
|
f'Received broker trade event:\n'
|
||||||
f'{fmsg}'
|
f'{fmsg}'
|
||||||
)
|
)
|
||||||
match brokerd_msg:
|
status_msg: Optional[Status] = None
|
||||||
|
|
||||||
|
match brokerd_msg:
|
||||||
# BrokerdPosition
|
# BrokerdPosition
|
||||||
case {
|
case {
|
||||||
'name': 'position',
|
'name': 'position',
|
||||||
|
@ -866,6 +867,7 @@ async def translate_and_relay_brokerd_events(
|
||||||
}:
|
}:
|
||||||
log.error(f'Broker error:\n{fmsg}')
|
log.error(f'Broker error:\n{fmsg}')
|
||||||
# XXX: we presume the brokerd cancels its own order
|
# XXX: we presume the brokerd cancels its own order
|
||||||
|
continue
|
||||||
|
|
||||||
# TOO FAST ``BrokerdStatus`` that arrives
|
# TOO FAST ``BrokerdStatus`` that arrives
|
||||||
# before the ``BrokerdAck``.
|
# before the ``BrokerdAck``.
|
||||||
|
@ -894,8 +896,8 @@ async def translate_and_relay_brokerd_events(
|
||||||
raise ValueError(f'Brokerd message {brokerd_msg} is invalid')
|
raise ValueError(f'Brokerd message {brokerd_msg} is invalid')
|
||||||
|
|
||||||
# XXX: ugh sometimes we don't access it?
|
# XXX: ugh sometimes we don't access it?
|
||||||
if status_msg:
|
# if status_msg is not None:
|
||||||
del status_msg
|
# del status_msg
|
||||||
|
|
||||||
# TODO: do we want this to keep things cleaned up?
|
# TODO: do we want this to keep things cleaned up?
|
||||||
# it might require a special status from brokerd to affirm the
|
# it might require a special status from brokerd to affirm the
|
||||||
|
@ -1107,7 +1109,7 @@ async def process_client_order_cmds(
|
||||||
# sometimes the real-time feed hasn't come up
|
# sometimes the real-time feed hasn't come up
|
||||||
# so just pull from the latest history.
|
# so just pull from the latest history.
|
||||||
if isnan(last):
|
if isnan(last):
|
||||||
last = feed.shm.array[-1]['close']
|
last = feed.rt_shm.array[-1]['close']
|
||||||
|
|
||||||
pred = mk_check(trigger_price, last, action)
|
pred = mk_check(trigger_price, last, action)
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,9 @@ if TYPE_CHECKING:
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
_default_delay_s: float = 1.0
|
||||||
|
|
||||||
|
|
||||||
class sampler:
|
class sampler:
|
||||||
'''
|
'''
|
||||||
Global sampling engine registry.
|
Global sampling engine registry.
|
||||||
|
@ -104,14 +107,18 @@ async def increment_ohlc_buffer(
|
||||||
# TODO: do we want to support dynamically
|
# TODO: do we want to support dynamically
|
||||||
# adding a "lower" lowest increment period?
|
# adding a "lower" lowest increment period?
|
||||||
await trio.sleep(ad)
|
await trio.sleep(ad)
|
||||||
total_s += lowest
|
total_s += delay_s
|
||||||
|
|
||||||
# increment all subscribed shm arrays
|
# increment all subscribed shm arrays
|
||||||
# TODO:
|
# TODO:
|
||||||
# - this in ``numba``
|
# - this in ``numba``
|
||||||
# - just lookup shms for this step instead of iterating?
|
# - just lookup shms for this step instead of iterating?
|
||||||
for delay_s, shms in sampler.ohlcv_shms.items():
|
for this_delay_s, shms in sampler.ohlcv_shms.items():
|
||||||
if total_s % delay_s != 0:
|
|
||||||
|
# short-circuit on any not-ready because slower sample
|
||||||
|
# rate consuming shm buffers.
|
||||||
|
if total_s % this_delay_s != 0:
|
||||||
|
# print(f'skipping `{this_delay_s}s` sample update')
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# TODO: ``numba`` this!
|
# TODO: ``numba`` this!
|
||||||
|
@ -130,7 +137,7 @@ async def increment_ohlc_buffer(
|
||||||
# this copies non-std fields (eg. vwap) from the last datum
|
# this copies non-std fields (eg. vwap) from the last datum
|
||||||
last[
|
last[
|
||||||
['time', 'volume', 'open', 'high', 'low', 'close']
|
['time', 'volume', 'open', 'high', 'low', 'close']
|
||||||
][0] = (t + delay_s, 0, close, close, close, close)
|
][0] = (t + this_delay_s, 0, close, close, close, close)
|
||||||
|
|
||||||
# write to the buffer
|
# write to the buffer
|
||||||
shm.push(last)
|
shm.push(last)
|
||||||
|
@ -152,7 +159,6 @@ async def broadcast(
|
||||||
|
|
||||||
'''
|
'''
|
||||||
subs = sampler.subscribers.get(delay_s, ())
|
subs = sampler.subscribers.get(delay_s, ())
|
||||||
|
|
||||||
first = last = -1
|
first = last = -1
|
||||||
|
|
||||||
if shm is None:
|
if shm is None:
|
||||||
|
@ -221,7 +227,8 @@ async def iter_ohlc_periods(
|
||||||
async def sample_and_broadcast(
|
async def sample_and_broadcast(
|
||||||
|
|
||||||
bus: _FeedsBus, # noqa
|
bus: _FeedsBus, # noqa
|
||||||
shm: ShmArray,
|
rt_shm: ShmArray,
|
||||||
|
hist_shm: ShmArray,
|
||||||
quote_stream: trio.abc.ReceiveChannel,
|
quote_stream: trio.abc.ReceiveChannel,
|
||||||
brokername: str,
|
brokername: str,
|
||||||
sum_tick_vlm: bool = True,
|
sum_tick_vlm: bool = True,
|
||||||
|
@ -257,8 +264,12 @@ async def sample_and_broadcast(
|
||||||
|
|
||||||
last = tick['price']
|
last = tick['price']
|
||||||
|
|
||||||
|
# more compact inline-way to do this assignment
|
||||||
|
# to both buffers?
|
||||||
|
for shm in [rt_shm, hist_shm]:
|
||||||
# update last entry
|
# update last entry
|
||||||
# benchmarked in the 4-5 us range
|
# benchmarked in the 4-5 us range
|
||||||
|
# for shm in [rt_shm, hist_shm]:
|
||||||
o, high, low, v = shm.array[-1][
|
o, high, low, v = shm.array[-1][
|
||||||
['open', 'high', 'low', 'volume']
|
['open', 'high', 'low', 'volume']
|
||||||
]
|
]
|
||||||
|
|
|
@ -56,6 +56,7 @@ from ._sharedmem import (
|
||||||
maybe_open_shm_array,
|
maybe_open_shm_array,
|
||||||
attach_shm_array,
|
attach_shm_array,
|
||||||
ShmArray,
|
ShmArray,
|
||||||
|
_secs_in_day,
|
||||||
)
|
)
|
||||||
from .ingest import get_ingestormod
|
from .ingest import get_ingestormod
|
||||||
from .types import Struct
|
from .types import Struct
|
||||||
|
@ -72,6 +73,7 @@ from ._sampling import (
|
||||||
iter_ohlc_periods,
|
iter_ohlc_periods,
|
||||||
sample_and_broadcast,
|
sample_and_broadcast,
|
||||||
uniform_rate_send,
|
uniform_rate_send,
|
||||||
|
_default_delay_s,
|
||||||
)
|
)
|
||||||
from ..brokers._util import (
|
from ..brokers._util import (
|
||||||
NoData,
|
NoData,
|
||||||
|
@ -256,7 +258,7 @@ async def start_backfill(
|
||||||
write_tsdb: bool = True,
|
write_tsdb: bool = True,
|
||||||
tsdb_is_up: bool = False,
|
tsdb_is_up: bool = False,
|
||||||
|
|
||||||
task_status: TaskStatus[trio.CancelScope] = trio.TASK_STATUS_IGNORED,
|
task_status: TaskStatus[tuple] = trio.TASK_STATUS_IGNORED,
|
||||||
|
|
||||||
) -> int:
|
) -> int:
|
||||||
|
|
||||||
|
@ -294,7 +296,7 @@ async def start_backfill(
|
||||||
bf_done = trio.Event()
|
bf_done = trio.Event()
|
||||||
|
|
||||||
# let caller unblock and deliver latest history frame
|
# let caller unblock and deliver latest history frame
|
||||||
task_status.started((shm, start_dt, end_dt, bf_done))
|
task_status.started((start_dt, end_dt, bf_done))
|
||||||
|
|
||||||
# based on the sample step size, maybe load a certain amount history
|
# based on the sample step size, maybe load a certain amount history
|
||||||
if last_tsdb_dt is None:
|
if last_tsdb_dt is None:
|
||||||
|
@ -544,7 +546,6 @@ async def start_backfill(
|
||||||
)
|
)
|
||||||
frames.pop(epoch)
|
frames.pop(epoch)
|
||||||
continue
|
continue
|
||||||
# await tractor.breakpoint()
|
|
||||||
|
|
||||||
if diff > step_size_s:
|
if diff > step_size_s:
|
||||||
|
|
||||||
|
@ -672,8 +673,8 @@ async def manage_history(
|
||||||
'''
|
'''
|
||||||
# (maybe) allocate shm array for this broker/symbol which will
|
# (maybe) allocate shm array for this broker/symbol which will
|
||||||
# be used for fast near-term history capture and processing.
|
# be used for fast near-term history capture and processing.
|
||||||
shm, opened = maybe_open_shm_array(
|
hist_shm, opened = maybe_open_shm_array(
|
||||||
key=fqsn,
|
key=f'{fqsn}_hist',
|
||||||
|
|
||||||
# use any broker defined ohlc dtype:
|
# use any broker defined ohlc dtype:
|
||||||
dtype=getattr(mod, '_ohlc_dtype', base_iohlc_dtype),
|
dtype=getattr(mod, '_ohlc_dtype', base_iohlc_dtype),
|
||||||
|
@ -687,6 +688,21 @@ async def manage_history(
|
||||||
"Persistent shm for sym was already open?!"
|
"Persistent shm for sym was already open?!"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
rt_shm, opened = maybe_open_shm_array(
|
||||||
|
key=f'{fqsn}_rt',
|
||||||
|
|
||||||
|
# use any broker defined ohlc dtype:
|
||||||
|
dtype=getattr(mod, '_ohlc_dtype', base_iohlc_dtype),
|
||||||
|
|
||||||
|
# we expect the sub-actor to write
|
||||||
|
readonly=False,
|
||||||
|
size=3*_secs_in_day,
|
||||||
|
)
|
||||||
|
if not opened:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Persistent shm for sym was already open?!"
|
||||||
|
)
|
||||||
|
|
||||||
log.info('Scanning for existing `marketstored`')
|
log.info('Scanning for existing `marketstored`')
|
||||||
|
|
||||||
is_up = await check_for_service('marketstored')
|
is_up = await check_for_service('marketstored')
|
||||||
|
@ -714,7 +730,6 @@ async def manage_history(
|
||||||
|
|
||||||
broker, symbol, expiry = unpack_fqsn(fqsn)
|
broker, symbol, expiry = unpack_fqsn(fqsn)
|
||||||
(
|
(
|
||||||
shm,
|
|
||||||
latest_start_dt,
|
latest_start_dt,
|
||||||
latest_end_dt,
|
latest_end_dt,
|
||||||
bf_done,
|
bf_done,
|
||||||
|
@ -723,14 +738,14 @@ async def manage_history(
|
||||||
start_backfill,
|
start_backfill,
|
||||||
mod,
|
mod,
|
||||||
bfqsn,
|
bfqsn,
|
||||||
shm,
|
hist_shm,
|
||||||
last_tsdb_dt=last_tsdb_dt,
|
last_tsdb_dt=last_tsdb_dt,
|
||||||
tsdb_is_up=True,
|
tsdb_is_up=True,
|
||||||
storage=storage,
|
storage=storage,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# if len(shm.array) < 2:
|
# if len(hist_shm.array) < 2:
|
||||||
# TODO: there's an edge case here to solve where if the last
|
# TODO: there's an edge case here to solve where if the last
|
||||||
# frame before market close (at least on ib) was pushed and
|
# frame before market close (at least on ib) was pushed and
|
||||||
# there was only "1 new" row pushed from the first backfill
|
# there was only "1 new" row pushed from the first backfill
|
||||||
|
@ -740,7 +755,7 @@ async def manage_history(
|
||||||
# the tsdb series and stash that somewhere as meta data on
|
# the tsdb series and stash that somewhere as meta data on
|
||||||
# the shm buffer?.. no se.
|
# the shm buffer?.. no se.
|
||||||
|
|
||||||
task_status.started(shm)
|
task_status.started((hist_shm, rt_shm))
|
||||||
some_data_ready.set()
|
some_data_ready.set()
|
||||||
|
|
||||||
await bf_done.wait()
|
await bf_done.wait()
|
||||||
|
@ -758,7 +773,7 @@ async def manage_history(
|
||||||
# TODO: see if there's faster multi-field reads:
|
# TODO: see if there's faster multi-field reads:
|
||||||
# https://numpy.org/doc/stable/user/basics.rec.html#accessing-multiple-fields
|
# https://numpy.org/doc/stable/user/basics.rec.html#accessing-multiple-fields
|
||||||
# re-index with a `time` and index field
|
# re-index with a `time` and index field
|
||||||
prepend_start = shm._first.value
|
prepend_start = hist_shm._first.value
|
||||||
|
|
||||||
# sanity check on most-recent-data loading
|
# sanity check on most-recent-data loading
|
||||||
assert prepend_start > dt_diff_s
|
assert prepend_start > dt_diff_s
|
||||||
|
@ -768,7 +783,7 @@ async def manage_history(
|
||||||
fastest = history[0]
|
fastest = history[0]
|
||||||
to_push = fastest[:prepend_start]
|
to_push = fastest[:prepend_start]
|
||||||
|
|
||||||
shm.push(
|
hist_shm.push(
|
||||||
to_push,
|
to_push,
|
||||||
|
|
||||||
# insert the history pre a "days worth" of samples
|
# insert the history pre a "days worth" of samples
|
||||||
|
@ -784,7 +799,7 @@ async def manage_history(
|
||||||
count = 0
|
count = 0
|
||||||
end = fastest['Epoch'][0]
|
end = fastest['Epoch'][0]
|
||||||
|
|
||||||
while shm._first.value > 0:
|
while hist_shm._first.value > 0:
|
||||||
count += 1
|
count += 1
|
||||||
series = await storage.read_ohlcv(
|
series = await storage.read_ohlcv(
|
||||||
fqsn,
|
fqsn,
|
||||||
|
@ -796,7 +811,7 @@ async def manage_history(
|
||||||
prepend_start -= len(to_push)
|
prepend_start -= len(to_push)
|
||||||
to_push = fastest[:prepend_start]
|
to_push = fastest[:prepend_start]
|
||||||
|
|
||||||
shm.push(
|
hist_shm.push(
|
||||||
to_push,
|
to_push,
|
||||||
|
|
||||||
# insert the history pre a "days worth" of samples
|
# insert the history pre a "days worth" of samples
|
||||||
|
@ -840,12 +855,12 @@ async def manage_history(
|
||||||
start_backfill,
|
start_backfill,
|
||||||
mod,
|
mod,
|
||||||
bfqsn,
|
bfqsn,
|
||||||
shm,
|
hist_shm,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# yield back after client connect with filled shm
|
# yield back after client connect with filled shm
|
||||||
task_status.started(shm)
|
task_status.started((hist_shm, rt_shm))
|
||||||
|
|
||||||
# indicate to caller that feed can be delivered to
|
# indicate to caller that feed can be delivered to
|
||||||
# remote requesting client since we've loaded history
|
# remote requesting client since we've loaded history
|
||||||
|
@ -891,7 +906,7 @@ async def allocate_persistent_feed(
|
||||||
|
|
||||||
# mem chan handed to broker backend so it can push real-time
|
# mem chan handed to broker backend so it can push real-time
|
||||||
# quotes to this task for sampling and history storage (see below).
|
# quotes to this task for sampling and history storage (see below).
|
||||||
send, quote_stream = trio.open_memory_channel(10)
|
send, quote_stream = trio.open_memory_channel(616)
|
||||||
|
|
||||||
# data sync signals for both history loading and market quotes
|
# data sync signals for both history loading and market quotes
|
||||||
some_data_ready = trio.Event()
|
some_data_ready = trio.Event()
|
||||||
|
@ -922,7 +937,7 @@ async def allocate_persistent_feed(
|
||||||
# https://github.com/python-trio/trio/issues/2258
|
# https://github.com/python-trio/trio/issues/2258
|
||||||
# bus.nursery.start_soon(
|
# bus.nursery.start_soon(
|
||||||
# await bus.start_task(
|
# await bus.start_task(
|
||||||
shm = await bus.nursery.start(
|
hist_shm, rt_shm = await bus.nursery.start(
|
||||||
manage_history,
|
manage_history,
|
||||||
mod,
|
mod,
|
||||||
bus,
|
bus,
|
||||||
|
@ -935,7 +950,9 @@ async def allocate_persistent_feed(
|
||||||
# can read directly from the memory which will be written by
|
# can read directly from the memory which will be written by
|
||||||
# this task.
|
# this task.
|
||||||
msg = init_msg[symbol]
|
msg = init_msg[symbol]
|
||||||
msg['shm_token'] = shm.token
|
msg['hist_shm_token'] = hist_shm.token
|
||||||
|
msg['startup_hist_index'] = hist_shm.index - 1
|
||||||
|
msg['rt_shm_token'] = rt_shm.token
|
||||||
|
|
||||||
# true fqsn
|
# true fqsn
|
||||||
fqsn = '.'.join((bfqsn, brokername))
|
fqsn = '.'.join((bfqsn, brokername))
|
||||||
|
@ -971,7 +988,25 @@ async def allocate_persistent_feed(
|
||||||
# for ambiguous names we simply apply the retreived
|
# for ambiguous names we simply apply the retreived
|
||||||
# feed to that name (for now).
|
# feed to that name (for now).
|
||||||
|
|
||||||
# task_status.started((init_msg, generic_first_quotes))
|
sampler.ohlcv_shms.setdefault(
|
||||||
|
1,
|
||||||
|
[]
|
||||||
|
).append(rt_shm)
|
||||||
|
ohlckeys = ['open', 'high', 'low', 'close']
|
||||||
|
|
||||||
|
# set the rt (hft) shm array as append only
|
||||||
|
# (for now).
|
||||||
|
rt_shm._first.value = 0
|
||||||
|
rt_shm._last.value = 0
|
||||||
|
|
||||||
|
# push last sample from history to rt buffer just as a filler datum
|
||||||
|
# but we don't want a history sized datum outlier so set vlm to zero
|
||||||
|
# and ohlc to the close value.
|
||||||
|
rt_shm.push(hist_shm.array[-2:-1])
|
||||||
|
|
||||||
|
rt_shm.array[ohlckeys] = hist_shm.array['close'][-1]
|
||||||
|
rt_shm._array['volume'] = 0
|
||||||
|
|
||||||
task_status.started()
|
task_status.started()
|
||||||
|
|
||||||
if not start_stream:
|
if not start_stream:
|
||||||
|
@ -983,14 +1018,18 @@ async def allocate_persistent_feed(
|
||||||
|
|
||||||
# start shm incrementer task for OHLC style sampling
|
# start shm incrementer task for OHLC style sampling
|
||||||
# at the current detected step period.
|
# at the current detected step period.
|
||||||
times = shm.array['time']
|
times = hist_shm.array['time']
|
||||||
delay_s = times[-1] - times[times != times[-1]][-1]
|
delay_s = times[-1] - times[times != times[-1]][-1]
|
||||||
|
sampler.ohlcv_shms.setdefault(delay_s, []).append(hist_shm)
|
||||||
|
|
||||||
sampler.ohlcv_shms.setdefault(delay_s, []).append(shm)
|
# create buffer a single incrementer task broker backend
|
||||||
if sampler.incrementers.get(delay_s) is None:
|
# (aka `brokerd`) using the lowest sampler period.
|
||||||
|
# await tractor.breakpoint()
|
||||||
|
# for delay_s in sampler.ohlcv_shms:
|
||||||
|
if sampler.incrementers.get(_default_delay_s) is None:
|
||||||
await bus.start_task(
|
await bus.start_task(
|
||||||
increment_ohlc_buffer,
|
increment_ohlc_buffer,
|
||||||
delay_s,
|
_default_delay_s,
|
||||||
)
|
)
|
||||||
|
|
||||||
sum_tick_vlm: bool = init_msg.get(
|
sum_tick_vlm: bool = init_msg.get(
|
||||||
|
@ -1001,7 +1040,8 @@ async def allocate_persistent_feed(
|
||||||
try:
|
try:
|
||||||
await sample_and_broadcast(
|
await sample_and_broadcast(
|
||||||
bus,
|
bus,
|
||||||
shm,
|
rt_shm,
|
||||||
|
hist_shm,
|
||||||
quote_stream,
|
quote_stream,
|
||||||
brokername,
|
brokername,
|
||||||
sum_tick_vlm
|
sum_tick_vlm
|
||||||
|
@ -1164,34 +1204,6 @@ async def open_feed_bus(
|
||||||
log.warning(f'{sub} for {symbol} was already removed?')
|
log.warning(f'{sub} for {symbol} was already removed?')
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def open_sample_step_stream(
|
|
||||||
portal: tractor.Portal,
|
|
||||||
delay_s: int,
|
|
||||||
|
|
||||||
) -> tractor.ReceiveMsgStream:
|
|
||||||
|
|
||||||
# XXX: this should be singleton on a host,
|
|
||||||
# a lone broker-daemon per provider should be
|
|
||||||
# created for all practical purposes
|
|
||||||
async with maybe_open_context(
|
|
||||||
acm_func=partial(
|
|
||||||
portal.open_context,
|
|
||||||
iter_ohlc_periods,
|
|
||||||
),
|
|
||||||
kwargs={'delay_s': delay_s},
|
|
||||||
|
|
||||||
) as (cache_hit, (ctx, first)):
|
|
||||||
async with ctx.open_stream() as istream:
|
|
||||||
if cache_hit:
|
|
||||||
# add a new broadcast subscription for the quote stream
|
|
||||||
# if this feed is likely already in use
|
|
||||||
async with istream.subscribe() as bistream:
|
|
||||||
yield bistream
|
|
||||||
else:
|
|
||||||
yield istream
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Feed:
|
class Feed:
|
||||||
'''
|
'''
|
||||||
|
@ -1204,13 +1216,16 @@ class Feed:
|
||||||
|
|
||||||
'''
|
'''
|
||||||
name: str
|
name: str
|
||||||
shm: ShmArray
|
hist_shm: ShmArray
|
||||||
|
rt_shm: ShmArray
|
||||||
mod: ModuleType
|
mod: ModuleType
|
||||||
first_quotes: dict # symbol names to first quote dicts
|
first_quotes: dict # symbol names to first quote dicts
|
||||||
_portal: tractor.Portal
|
_portal: tractor.Portal
|
||||||
stream: trio.abc.ReceiveChannel[dict[str, Any]]
|
stream: trio.abc.ReceiveChannel[dict[str, Any]]
|
||||||
status: dict[str, Any]
|
status: dict[str, Any]
|
||||||
|
|
||||||
|
startup_hist_index: int = 0
|
||||||
|
|
||||||
throttle_rate: Optional[int] = None
|
throttle_rate: Optional[int] = None
|
||||||
|
|
||||||
_trade_stream: Optional[AsyncIterator[dict[str, Any]]] = None
|
_trade_stream: Optional[AsyncIterator[dict[str, Any]]] = None
|
||||||
|
@ -1230,16 +1245,27 @@ class Feed:
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def index_stream(
|
async def index_stream(
|
||||||
self,
|
self,
|
||||||
delay_s: Optional[int] = None
|
delay_s: int = 1,
|
||||||
|
|
||||||
) -> AsyncIterator[int]:
|
) -> AsyncIterator[int]:
|
||||||
|
|
||||||
delay_s = delay_s or self._max_sample_rate
|
# XXX: this should be singleton on a host,
|
||||||
|
# a lone broker-daemon per provider should be
|
||||||
async with open_sample_step_stream(
|
# created for all practical purposes
|
||||||
self.portal,
|
async with maybe_open_context(
|
||||||
delay_s,
|
acm_func=partial(
|
||||||
) as istream:
|
self.portal.open_context,
|
||||||
|
iter_ohlc_periods,
|
||||||
|
),
|
||||||
|
kwargs={'delay_s': delay_s},
|
||||||
|
) as (cache_hit, (ctx, first)):
|
||||||
|
async with ctx.open_stream() as istream:
|
||||||
|
if cache_hit:
|
||||||
|
# add a new broadcast subscription for the quote stream
|
||||||
|
# if this feed is likely already in use
|
||||||
|
async with istream.subscribe() as bistream:
|
||||||
|
yield bistream
|
||||||
|
else:
|
||||||
yield istream
|
yield istream
|
||||||
|
|
||||||
async def pause(self) -> None:
|
async def pause(self) -> None:
|
||||||
|
@ -1248,6 +1274,34 @@ class Feed:
|
||||||
async def resume(self) -> None:
|
async def resume(self) -> None:
|
||||||
await self.stream.send('resume')
|
await self.stream.send('resume')
|
||||||
|
|
||||||
|
def get_ds_info(
|
||||||
|
self,
|
||||||
|
) -> tuple[float, float, float]:
|
||||||
|
'''
|
||||||
|
Compute the "downsampling" ratio info between the historical shm
|
||||||
|
buffer and the real-time (HFT) one.
|
||||||
|
|
||||||
|
Return a tuple of the fast sample period, historical sample
|
||||||
|
period and ratio between them.
|
||||||
|
|
||||||
|
'''
|
||||||
|
times = self.hist_shm.array['time']
|
||||||
|
end = pendulum.from_timestamp(times[-1])
|
||||||
|
start = pendulum.from_timestamp(times[times != times[-1]][-1])
|
||||||
|
hist_step_size_s = (end - start).seconds
|
||||||
|
|
||||||
|
times = self.rt_shm.array['time']
|
||||||
|
end = pendulum.from_timestamp(times[-1])
|
||||||
|
start = pendulum.from_timestamp(times[times != times[-1]][-1])
|
||||||
|
rt_step_size_s = (end - start).seconds
|
||||||
|
|
||||||
|
ratio = hist_step_size_s / rt_step_size_s
|
||||||
|
return (
|
||||||
|
rt_step_size_s,
|
||||||
|
hist_step_size_s,
|
||||||
|
ratio,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def install_brokerd_search(
|
async def install_brokerd_search(
|
||||||
|
@ -1337,21 +1391,29 @@ async def open_feed(
|
||||||
) as stream,
|
) as stream,
|
||||||
|
|
||||||
):
|
):
|
||||||
|
init = init_msg[bfqsn]
|
||||||
# we can only read from shm
|
# we can only read from shm
|
||||||
shm = attach_shm_array(
|
hist_shm = attach_shm_array(
|
||||||
token=init_msg[bfqsn]['shm_token'],
|
token=init['hist_shm_token'],
|
||||||
readonly=True,
|
readonly=True,
|
||||||
)
|
)
|
||||||
|
rt_shm = attach_shm_array(
|
||||||
|
token=init['rt_shm_token'],
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
|
||||||
assert fqsn in first_quotes
|
assert fqsn in first_quotes
|
||||||
|
|
||||||
feed = Feed(
|
feed = Feed(
|
||||||
name=brokername,
|
name=brokername,
|
||||||
shm=shm,
|
hist_shm=hist_shm,
|
||||||
|
rt_shm=rt_shm,
|
||||||
mod=mod,
|
mod=mod,
|
||||||
first_quotes=first_quotes,
|
first_quotes=first_quotes,
|
||||||
stream=stream,
|
stream=stream,
|
||||||
_portal=portal,
|
_portal=portal,
|
||||||
status={},
|
status={},
|
||||||
|
startup_hist_index=init['startup_hist_index'],
|
||||||
throttle_rate=tick_throttle,
|
throttle_rate=tick_throttle,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1364,7 +1426,7 @@ async def open_feed(
|
||||||
'actor_name': feed.portal.channel.uid[0],
|
'actor_name': feed.portal.channel.uid[0],
|
||||||
'host': host,
|
'host': host,
|
||||||
'port': port,
|
'port': port,
|
||||||
'shm': f'{humanize(feed.shm._shm.size)}',
|
'shm': f'{humanize(feed.hist_shm._shm.size)}',
|
||||||
'throttle_rate': feed.throttle_rate,
|
'throttle_rate': feed.throttle_rate,
|
||||||
})
|
})
|
||||||
feed.status.update(init_msg.pop('status', {}))
|
feed.status.update(init_msg.pop('status', {}))
|
||||||
|
@ -1382,7 +1444,11 @@ async def open_feed(
|
||||||
feed.symbols[sym] = symbol
|
feed.symbols[sym] = 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
|
||||||
shm_token = data['shm_token']
|
for shm_key, shm in [
|
||||||
|
('rt_shm_token', rt_shm),
|
||||||
|
('hist_shm_token', hist_shm),
|
||||||
|
]:
|
||||||
|
shm_token = data[shm_key]
|
||||||
|
|
||||||
# XXX: msgspec won't relay through the tuples XD
|
# XXX: msgspec won't relay through the tuples XD
|
||||||
shm_token['dtype_descr'] = tuple(
|
shm_token['dtype_descr'] = tuple(
|
||||||
|
|
|
@ -37,6 +37,7 @@ from .. import data
|
||||||
from ..data import attach_shm_array
|
from ..data import attach_shm_array
|
||||||
from ..data.feed import Feed
|
from ..data.feed import Feed
|
||||||
from ..data._sharedmem import ShmArray
|
from ..data._sharedmem import ShmArray
|
||||||
|
from ..data._sampling import _default_delay_s
|
||||||
from ..data._source import Symbol
|
from ..data._source import Symbol
|
||||||
from ._api import (
|
from ._api import (
|
||||||
Fsp,
|
Fsp,
|
||||||
|
@ -105,7 +106,7 @@ async def fsp_compute(
|
||||||
filter_quotes_by_sym(fqsn, quote_stream),
|
filter_quotes_by_sym(fqsn, quote_stream),
|
||||||
|
|
||||||
# XXX: currently the ``ohlcv`` arg
|
# XXX: currently the ``ohlcv`` arg
|
||||||
feed.shm,
|
feed.rt_shm,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Conduct a single iteration of fsp with historical bars input
|
# Conduct a single iteration of fsp with historical bars input
|
||||||
|
@ -313,7 +314,7 @@ async def cascade(
|
||||||
|
|
||||||
profiler(f'{func}: feed up')
|
profiler(f'{func}: feed up')
|
||||||
|
|
||||||
assert src.token == feed.shm.token
|
assert src.token == feed.rt_shm.token
|
||||||
# last_len = new_len = len(src.array)
|
# last_len = new_len = len(src.array)
|
||||||
|
|
||||||
func_name = func.__name__
|
func_name = func.__name__
|
||||||
|
@ -420,7 +421,11 @@ async def cascade(
|
||||||
# detect sample period step for subscription to increment
|
# detect sample period step for subscription to increment
|
||||||
# signal
|
# signal
|
||||||
times = src.array['time']
|
times = src.array['time']
|
||||||
|
if len(times) > 1:
|
||||||
delay_s = times[-1] - times[times != times[-1]][-1]
|
delay_s = times[-1] - times[times != times[-1]][-1]
|
||||||
|
else:
|
||||||
|
# our default "HFT" sample rate.
|
||||||
|
delay_s = _default_delay_s
|
||||||
|
|
||||||
# Increment the underlying shared memory buffer on every
|
# Increment the underlying shared memory buffer on every
|
||||||
# "increment" msg received from the underlying data feed.
|
# "increment" msg received from the underlying data feed.
|
||||||
|
@ -431,7 +436,8 @@ async def cascade(
|
||||||
profiler(f'{func_name}: sample stream up')
|
profiler(f'{func_name}: sample stream up')
|
||||||
profiler.finish()
|
profiler.finish()
|
||||||
|
|
||||||
async for _ in istream:
|
async for i in istream:
|
||||||
|
# log.runtime(f'FSP incrementing {i}')
|
||||||
|
|
||||||
# respawn the compute task if the source
|
# respawn the compute task if the source
|
||||||
# array has been updated such that we compute
|
# array has been updated such that we compute
|
||||||
|
|
|
@ -32,16 +32,22 @@ def mk_marker_path(
|
||||||
style: str,
|
style: str,
|
||||||
|
|
||||||
) -> QGraphicsPathItem:
|
) -> QGraphicsPathItem:
|
||||||
"""Add a marker to be displayed on the line wrapped in a ``QGraphicsPathItem``
|
'''
|
||||||
ready to be placed using scene coordinates (not view).
|
Add a marker to be displayed on the line wrapped in
|
||||||
|
a ``QGraphicsPathItem`` ready to be placed using scene coordinates
|
||||||
|
(not view).
|
||||||
|
|
||||||
**Arguments**
|
**Arguments**
|
||||||
style String indicating the style of marker to add:
|
style String indicating the style of marker to add:
|
||||||
``'<|'``, ``'|>'``, ``'>|'``, ``'|<'``, ``'<|>'``,
|
``'<|'``, ``'|>'``, ``'>|'``, ``'|<'``, ``'<|>'``,
|
||||||
``'>|<'``, ``'^'``, ``'v'``, ``'o'``
|
``'>|<'``, ``'^'``, ``'v'``, ``'o'``
|
||||||
size Size of the marker in pixels.
|
|
||||||
|
|
||||||
"""
|
This code is taken nearly verbatim from the
|
||||||
|
`InfiniteLine.addMarker()` method but does not attempt do be aware
|
||||||
|
of low(er) level graphics controls and expects for the output
|
||||||
|
polygon to be applied to a ``QGraphicsPathItem``.
|
||||||
|
|
||||||
|
'''
|
||||||
path = QtGui.QPainterPath()
|
path = QtGui.QPainterPath()
|
||||||
|
|
||||||
if style == 'o':
|
if style == 'o':
|
||||||
|
@ -87,7 +93,8 @@ def mk_marker_path(
|
||||||
|
|
||||||
|
|
||||||
class LevelMarker(QGraphicsPathItem):
|
class LevelMarker(QGraphicsPathItem):
|
||||||
'''An arrow marker path graphich which redraws itself
|
'''
|
||||||
|
An arrow marker path graphich which redraws itself
|
||||||
to the specified view coordinate level on each paint cycle.
|
to the specified view coordinate level on each paint cycle.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
@ -114,6 +121,7 @@ class LevelMarker(QGraphicsPathItem):
|
||||||
|
|
||||||
self.get_level = get_level
|
self.get_level = get_level
|
||||||
self._on_paint = on_paint
|
self._on_paint = on_paint
|
||||||
|
|
||||||
self.scene_x = lambda: chart.marker_right_points()[1]
|
self.scene_x = lambda: chart.marker_right_points()[1]
|
||||||
self.level: float = 0
|
self.level: float = 0
|
||||||
self.keep_in_view = keep_in_view
|
self.keep_in_view = keep_in_view
|
||||||
|
@ -149,12 +157,9 @@ class LevelMarker(QGraphicsPathItem):
|
||||||
def w(self) -> float:
|
def w(self) -> float:
|
||||||
return self.path_br().width()
|
return self.path_br().width()
|
||||||
|
|
||||||
def position_in_view(
|
def position_in_view(self) -> None:
|
||||||
self,
|
'''
|
||||||
# level: float,
|
Show a pp off-screen indicator for a level label.
|
||||||
|
|
||||||
) -> None:
|
|
||||||
'''Show a pp off-screen indicator for a level label.
|
|
||||||
|
|
||||||
This is like in fps games where you have a gps "nav" indicator
|
This is like in fps games where you have a gps "nav" indicator
|
||||||
but your teammate is outside the range of view, except in 2D, on
|
but your teammate is outside the range of view, except in 2D, on
|
||||||
|
@ -162,7 +167,6 @@ class LevelMarker(QGraphicsPathItem):
|
||||||
|
|
||||||
'''
|
'''
|
||||||
level = self.get_level()
|
level = self.get_level()
|
||||||
|
|
||||||
view = self.chart.getViewBox()
|
view = self.chart.getViewBox()
|
||||||
vr = view.state['viewRange']
|
vr = view.state['viewRange']
|
||||||
ymn, ymx = vr[1]
|
ymn, ymx = vr[1]
|
||||||
|
@ -186,7 +190,6 @@ class LevelMarker(QGraphicsPathItem):
|
||||||
)
|
)
|
||||||
|
|
||||||
elif level < ymn: # pin to bottom of view
|
elif level < ymn: # pin to bottom of view
|
||||||
|
|
||||||
self.setPos(
|
self.setPos(
|
||||||
QPointF(
|
QPointF(
|
||||||
x,
|
x,
|
||||||
|
@ -211,7 +214,8 @@ class LevelMarker(QGraphicsPathItem):
|
||||||
w: QtWidgets.QWidget
|
w: QtWidgets.QWidget
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''Core paint which we override to always update
|
'''
|
||||||
|
Core paint which we override to always update
|
||||||
our marker position in scene coordinates from a
|
our marker position in scene coordinates from a
|
||||||
view cooridnate "level".
|
view cooridnate "level".
|
||||||
|
|
||||||
|
@ -235,11 +239,12 @@ def qgo_draw_markers(
|
||||||
right_offset: float,
|
right_offset: float,
|
||||||
|
|
||||||
) -> float:
|
) -> float:
|
||||||
"""Paint markers in ``pg.GraphicsItem`` style by first
|
'''
|
||||||
|
Paint markers in ``pg.GraphicsItem`` style by first
|
||||||
removing the view transform for the painter, drawing the markers
|
removing the view transform for the painter, drawing the markers
|
||||||
in scene coords, then restoring the view coords.
|
in scene coords, then restoring the view coords.
|
||||||
|
|
||||||
"""
|
'''
|
||||||
# paint markers in native coordinate system
|
# paint markers in native coordinate system
|
||||||
orig_tr = p.transform()
|
orig_tr = p.transform()
|
||||||
|
|
||||||
|
|
|
@ -107,9 +107,8 @@ async def _async_main(
|
||||||
# setup search widget and focus main chart view at startup
|
# setup search widget and focus main chart view at startup
|
||||||
# search widget is a singleton alongside the godwidget
|
# search widget is a singleton alongside the godwidget
|
||||||
search = _search.SearchWidget(godwidget=godwidget)
|
search = _search.SearchWidget(godwidget=godwidget)
|
||||||
search.bar.unfocus()
|
# search.bar.unfocus()
|
||||||
|
# godwidget.hbox.addWidget(search)
|
||||||
godwidget.hbox.addWidget(search)
|
|
||||||
godwidget.search = search
|
godwidget.search = search
|
||||||
|
|
||||||
symbol, _, provider = sym.rpartition('.')
|
symbol, _, provider = sym.rpartition('.')
|
||||||
|
@ -178,6 +177,6 @@ def _main(
|
||||||
run_qtractor(
|
run_qtractor(
|
||||||
func=_async_main,
|
func=_async_main,
|
||||||
args=(sym, brokernames, piker_loglevel),
|
args=(sym, brokernames, piker_loglevel),
|
||||||
main_widget=GodWidget,
|
main_widget_type=GodWidget,
|
||||||
tractor_kwargs=tractor_kwargs,
|
tractor_kwargs=tractor_kwargs,
|
||||||
)
|
)
|
||||||
|
|
|
@ -19,7 +19,11 @@ High level chart-widget apis.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Optional, TYPE_CHECKING
|
from typing import (
|
||||||
|
Iterator,
|
||||||
|
Optional,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
)
|
||||||
|
|
||||||
from PyQt5 import QtCore, QtWidgets
|
from PyQt5 import QtCore, QtWidgets
|
||||||
from PyQt5.QtCore import (
|
from PyQt5.QtCore import (
|
||||||
|
@ -68,6 +72,7 @@ from ._forms import FieldsForm
|
||||||
from .._profile import pg_profile_enabled, ms_slower_then
|
from .._profile import pg_profile_enabled, ms_slower_then
|
||||||
from ._overlay import PlotItemOverlay
|
from ._overlay import PlotItemOverlay
|
||||||
from ._flows import Flow
|
from ._flows import Flow
|
||||||
|
from ._search import SearchWidget
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._display import DisplayState
|
from ._display import DisplayState
|
||||||
|
@ -85,6 +90,9 @@ class GodWidget(QWidget):
|
||||||
modify them.
|
modify them.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
search: SearchWidget
|
||||||
|
mode_name: str = 'god'
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
||||||
self,
|
self,
|
||||||
|
@ -94,6 +102,8 @@ class GodWidget(QWidget):
|
||||||
|
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.search: Optional[SearchWidget] = None
|
||||||
|
|
||||||
self.hbox = QHBoxLayout(self)
|
self.hbox = QHBoxLayout(self)
|
||||||
self.hbox.setContentsMargins(0, 0, 0, 0)
|
self.hbox.setContentsMargins(0, 0, 0, 0)
|
||||||
self.hbox.setSpacing(6)
|
self.hbox.setSpacing(6)
|
||||||
|
@ -115,7 +125,10 @@ class GodWidget(QWidget):
|
||||||
# self.vbox.addLayout(self.hbox)
|
# self.vbox.addLayout(self.hbox)
|
||||||
|
|
||||||
self._chart_cache: dict[str, LinkedSplits] = {}
|
self._chart_cache: dict[str, LinkedSplits] = {}
|
||||||
self.linkedsplits: Optional[LinkedSplits] = None
|
|
||||||
|
self.hist_linked: Optional[LinkedSplits] = None
|
||||||
|
self.rt_linked: Optional[LinkedSplits] = None
|
||||||
|
self._active_cursor: Optional[Cursor] = None
|
||||||
|
|
||||||
# assigned in the startup func `_async_main()`
|
# assigned in the startup func `_async_main()`
|
||||||
self._root_n: trio.Nursery = None
|
self._root_n: trio.Nursery = None
|
||||||
|
@ -123,6 +136,14 @@ class GodWidget(QWidget):
|
||||||
self._widgets: dict[str, QWidget] = {}
|
self._widgets: dict[str, QWidget] = {}
|
||||||
self._resizing: bool = False
|
self._resizing: bool = False
|
||||||
|
|
||||||
|
# TODO: do we need this, when would god get resized
|
||||||
|
# and the window does not? Never right?!
|
||||||
|
# self.reg_for_resize(self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def linkedsplits(self) -> LinkedSplits:
|
||||||
|
return self.rt_linked
|
||||||
|
|
||||||
# def init_timeframes_ui(self):
|
# def init_timeframes_ui(self):
|
||||||
# self.tf_layout = QHBoxLayout()
|
# self.tf_layout = QHBoxLayout()
|
||||||
# self.tf_layout.setSpacing(0)
|
# self.tf_layout.setSpacing(0)
|
||||||
|
@ -148,19 +169,19 @@ class GodWidget(QWidget):
|
||||||
def set_chart_symbol(
|
def set_chart_symbol(
|
||||||
self,
|
self,
|
||||||
symbol_key: str, # of form <fqsn>.<providername>
|
symbol_key: str, # of form <fqsn>.<providername>
|
||||||
linkedsplits: LinkedSplits, # type: ignore
|
all_linked: tuple[LinkedSplits, LinkedSplits], # type: ignore
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
# re-sort org cache symbol list in LIFO order
|
# re-sort org cache symbol list in LIFO order
|
||||||
cache = self._chart_cache
|
cache = self._chart_cache
|
||||||
cache.pop(symbol_key, None)
|
cache.pop(symbol_key, None)
|
||||||
cache[symbol_key] = linkedsplits
|
cache[symbol_key] = all_linked
|
||||||
|
|
||||||
def get_chart_symbol(
|
def get_chart_symbol(
|
||||||
self,
|
self,
|
||||||
symbol_key: str,
|
symbol_key: str,
|
||||||
|
|
||||||
) -> 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_symbol(
|
||||||
|
@ -182,28 +203,33 @@ class GodWidget(QWidget):
|
||||||
|
|
||||||
# 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([symbol_key, providername])
|
||||||
|
all_linked = self.get_chart_symbol(fqsn)
|
||||||
linkedsplits = self.get_chart_symbol(fqsn)
|
|
||||||
|
|
||||||
order_mode_started = trio.Event()
|
order_mode_started = trio.Event()
|
||||||
|
|
||||||
if not self.vbox.isEmpty():
|
if not self.vbox.isEmpty():
|
||||||
|
|
||||||
|
# XXX: seems to make switching slower?
|
||||||
|
# qframe = self.hist_linked.chart.qframe
|
||||||
|
# if qframe.sidepane is self.search:
|
||||||
|
# qframe.hbox.removeWidget(self.search)
|
||||||
|
|
||||||
|
for linked in [self.rt_linked, self.hist_linked]:
|
||||||
# XXX: this is CRITICAL especially with pixel buffer caching
|
# XXX: this is CRITICAL especially with pixel buffer caching
|
||||||
self.linkedsplits.hide()
|
linked.hide()
|
||||||
self.linkedsplits.unfocus()
|
linked.unfocus()
|
||||||
|
|
||||||
# XXX: pretty sure we don't need this
|
# XXX: pretty sure we don't need this
|
||||||
# remove any existing plots?
|
# remove any existing plots?
|
||||||
# XXX: ahh we might want to support cache unloading..
|
# XXX: ahh we might want to support cache unloading..
|
||||||
# self.vbox.removeWidget(self.linkedsplits)
|
# self.vbox.removeWidget(linked)
|
||||||
|
|
||||||
# switching to a new viewable chart
|
# switching to a new viewable chart
|
||||||
if linkedsplits is None or reset:
|
if all_linked is None or reset:
|
||||||
from ._display import display_symbol_data
|
from ._display import display_symbol_data
|
||||||
|
|
||||||
# we must load a fresh linked charts set
|
# we must load a fresh linked charts set
|
||||||
linkedsplits = LinkedSplits(self)
|
self.rt_linked = rt_charts = LinkedSplits(self)
|
||||||
|
self.hist_linked = hist_charts = LinkedSplits(self)
|
||||||
|
|
||||||
# spawn new task to start up and update new sub-chart instances
|
# spawn new task to start up and update new sub-chart instances
|
||||||
self._root_n.start_soon(
|
self._root_n.start_soon(
|
||||||
|
@ -215,33 +241,38 @@ class GodWidget(QWidget):
|
||||||
order_mode_started,
|
order_mode_started,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.set_chart_symbol(fqsn, linkedsplits)
|
# self.vbox.addWidget(hist_charts)
|
||||||
self.vbox.addWidget(linkedsplits)
|
self.vbox.addWidget(rt_charts)
|
||||||
|
self.set_chart_symbol(
|
||||||
|
fqsn,
|
||||||
|
(hist_charts, rt_charts),
|
||||||
|
)
|
||||||
|
|
||||||
|
for linked in [hist_charts, rt_charts]:
|
||||||
|
linked.show()
|
||||||
|
linked.focus()
|
||||||
|
|
||||||
linkedsplits.show()
|
|
||||||
linkedsplits.focus()
|
|
||||||
await trio.sleep(0)
|
await trio.sleep(0)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# symbol is already loaded and ems ready
|
# symbol is already loaded and ems ready
|
||||||
order_mode_started.set()
|
order_mode_started.set()
|
||||||
|
|
||||||
|
self.hist_linked, self.rt_linked = all_linked
|
||||||
|
|
||||||
|
for linked in all_linked:
|
||||||
# TODO:
|
# TODO:
|
||||||
# - we'll probably want per-instrument/provider state here?
|
# - we'll probably want per-instrument/provider state here?
|
||||||
# change the order config form over to the new chart
|
# change the order config form over to the new chart
|
||||||
|
|
||||||
# chart is already in memory so just focus it
|
# chart is already in memory so just focus it
|
||||||
linkedsplits.show()
|
linked.show()
|
||||||
linkedsplits.focus()
|
linked.focus()
|
||||||
linkedsplits.graphics_cycle()
|
linked.graphics_cycle()
|
||||||
await trio.sleep(0)
|
await trio.sleep(0)
|
||||||
|
|
||||||
# XXX: since the pp config is a singleton widget we have to
|
|
||||||
# also switch it over to the new chart's interal-layout
|
|
||||||
# self.linkedsplits.chart.qframe.hbox.removeWidget(self.pp_pane)
|
|
||||||
chart = linkedsplits.chart
|
|
||||||
|
|
||||||
# resume feeds *after* rendering chart view asap
|
# resume feeds *after* rendering chart view asap
|
||||||
|
chart = linked.chart
|
||||||
if chart:
|
if chart:
|
||||||
chart.resume_all_feeds()
|
chart.resume_all_feeds()
|
||||||
|
|
||||||
|
@ -249,10 +280,31 @@ class GodWidget(QWidget):
|
||||||
# last had the xlast in view, if so then shift so it's
|
# last had the xlast in view, if so then shift so it's
|
||||||
# still in view, if the user was viewing history then
|
# still in view, if the user was viewing history then
|
||||||
# do nothing yah?
|
# do nothing yah?
|
||||||
chart.default_view()
|
self.rt_linked.chart.default_view()
|
||||||
|
|
||||||
self.linkedsplits = linkedsplits
|
# if a history chart instance is already up then
|
||||||
symbol = linkedsplits.symbol
|
# set the search widget as its sidepane.
|
||||||
|
hist_chart = self.hist_linked.chart
|
||||||
|
if hist_chart:
|
||||||
|
hist_chart.qframe.set_sidepane(self.search)
|
||||||
|
|
||||||
|
# NOTE: this is really stupid/hard to follow.
|
||||||
|
# we have to reposition the active position nav
|
||||||
|
# **AFTER** applying the search bar as a sidepane
|
||||||
|
# to the newly switched to symbol.
|
||||||
|
await trio.sleep(0)
|
||||||
|
|
||||||
|
# TODO: probably stick this in some kinda `LooknFeel` API?
|
||||||
|
for tracker in self.rt_linked.mode.trackers.values():
|
||||||
|
pp_nav = tracker.nav
|
||||||
|
if tracker.live_pp.size:
|
||||||
|
pp_nav.show()
|
||||||
|
pp_nav.hide_info()
|
||||||
|
else:
|
||||||
|
pp_nav.hide()
|
||||||
|
|
||||||
|
# set window titlebar info
|
||||||
|
symbol = self.rt_linked.symbol
|
||||||
if symbol is not None:
|
if symbol is not None:
|
||||||
self.window.setWindowTitle(
|
self.window.setWindowTitle(
|
||||||
f'{symbol.front_fqsn()} '
|
f'{symbol.front_fqsn()} '
|
||||||
|
@ -269,11 +321,23 @@ class GodWidget(QWidget):
|
||||||
'''
|
'''
|
||||||
# go back to view-mode focus (aka chart focus)
|
# go back to view-mode focus (aka chart focus)
|
||||||
self.clearFocus()
|
self.clearFocus()
|
||||||
self.linkedsplits.chart.setFocus()
|
chart = self.rt_linked.chart
|
||||||
|
if chart:
|
||||||
|
chart.setFocus()
|
||||||
|
|
||||||
def resizeEvent(self, event: QtCore.QEvent) -> None:
|
def reg_for_resize(
|
||||||
|
self,
|
||||||
|
widget: QWidget,
|
||||||
|
) -> None:
|
||||||
|
getattr(widget, 'on_resize')
|
||||||
|
self._widgets[widget.mode_name] = widget
|
||||||
|
|
||||||
|
def on_win_resize(self, event: QtCore.QEvent) -> None:
|
||||||
'''
|
'''
|
||||||
Top level god widget resize handler.
|
Top level god widget handler from window (the real yaweh) resize
|
||||||
|
events such that any registered widgets which wish to be
|
||||||
|
notified are invoked using our pythonic `.on_resize()` method
|
||||||
|
api.
|
||||||
|
|
||||||
Where we do UX magic to make things not suck B)
|
Where we do UX magic to make things not suck B)
|
||||||
|
|
||||||
|
@ -289,6 +353,28 @@ class GodWidget(QWidget):
|
||||||
|
|
||||||
self._resizing = False
|
self._resizing = False
|
||||||
|
|
||||||
|
# on_resize = on_win_resize
|
||||||
|
|
||||||
|
def get_cursor(self) -> Cursor:
|
||||||
|
return self._active_cursor
|
||||||
|
|
||||||
|
def iter_linked(self) -> Iterator[LinkedSplits]:
|
||||||
|
for linked in [self.hist_linked, self.rt_linked]:
|
||||||
|
yield linked
|
||||||
|
|
||||||
|
def resize_all(self) -> None:
|
||||||
|
'''
|
||||||
|
Dynamic resize sequence: adjusts all sub-widgets/charts to
|
||||||
|
sensible default ratios of what space is detected as available
|
||||||
|
on the display / window.
|
||||||
|
|
||||||
|
'''
|
||||||
|
rt_linked = self.rt_linked
|
||||||
|
rt_linked.set_split_sizes()
|
||||||
|
self.rt_linked.resize_sidepanes()
|
||||||
|
self.hist_linked.resize_sidepanes(from_linked=rt_linked)
|
||||||
|
self.search.on_resize()
|
||||||
|
|
||||||
|
|
||||||
class ChartnPane(QFrame):
|
class ChartnPane(QFrame):
|
||||||
'''
|
'''
|
||||||
|
@ -301,9 +387,9 @@ class ChartnPane(QFrame):
|
||||||
https://doc.qt.io/qt-5/qwidget.html#composite-widgets
|
https://doc.qt.io/qt-5/qwidget.html#composite-widgets
|
||||||
|
|
||||||
'''
|
'''
|
||||||
sidepane: FieldsForm
|
sidepane: FieldsForm | SearchWidget
|
||||||
hbox: QHBoxLayout
|
hbox: QHBoxLayout
|
||||||
chart: Optional['ChartPlotWidget'] = None
|
chart: Optional[ChartPlotWidget] = None
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -315,7 +401,7 @@ class ChartnPane(QFrame):
|
||||||
|
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self.sidepane = sidepane
|
self._sidepane = sidepane
|
||||||
self.chart = None
|
self.chart = None
|
||||||
|
|
||||||
hbox = self.hbox = QHBoxLayout(self)
|
hbox = self.hbox = QHBoxLayout(self)
|
||||||
|
@ -323,6 +409,21 @@ class ChartnPane(QFrame):
|
||||||
hbox.setContentsMargins(0, 0, 0, 0)
|
hbox.setContentsMargins(0, 0, 0, 0)
|
||||||
hbox.setSpacing(3)
|
hbox.setSpacing(3)
|
||||||
|
|
||||||
|
def set_sidepane(
|
||||||
|
self,
|
||||||
|
sidepane: FieldsForm | SearchWidget,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
# add sidepane **after** chart; place it on axis side
|
||||||
|
self.hbox.addWidget(
|
||||||
|
sidepane,
|
||||||
|
alignment=Qt.AlignTop
|
||||||
|
)
|
||||||
|
self._sidepane = sidepane
|
||||||
|
|
||||||
|
def sidepane(self) -> FieldsForm | SearchWidget:
|
||||||
|
return self._sidepane
|
||||||
|
|
||||||
|
|
||||||
class LinkedSplits(QWidget):
|
class LinkedSplits(QWidget):
|
||||||
'''
|
'''
|
||||||
|
@ -357,6 +458,7 @@ class LinkedSplits(QWidget):
|
||||||
self.splitter = QSplitter(QtCore.Qt.Vertical)
|
self.splitter = QSplitter(QtCore.Qt.Vertical)
|
||||||
self.splitter.setMidLineWidth(0)
|
self.splitter.setMidLineWidth(0)
|
||||||
self.splitter.setHandleWidth(2)
|
self.splitter.setHandleWidth(2)
|
||||||
|
self.splitter.splitterMoved.connect(self.on_splitter_adjust)
|
||||||
|
|
||||||
self.layout = QVBoxLayout(self)
|
self.layout = QVBoxLayout(self)
|
||||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
@ -369,6 +471,16 @@ class LinkedSplits(QWidget):
|
||||||
|
|
||||||
self._symbol: Symbol = None
|
self._symbol: Symbol = None
|
||||||
|
|
||||||
|
def on_splitter_adjust(
|
||||||
|
self,
|
||||||
|
pos: int,
|
||||||
|
index: int,
|
||||||
|
) -> None:
|
||||||
|
# print(f'splitter moved pos:{pos}, index:{index}')
|
||||||
|
godw = self.godwidget
|
||||||
|
if self is godw.rt_linked:
|
||||||
|
godw.search.on_resize()
|
||||||
|
|
||||||
def graphics_cycle(self, **kwargs) -> None:
|
def graphics_cycle(self, **kwargs) -> None:
|
||||||
from . import _display
|
from . import _display
|
||||||
ds = self.display_state
|
ds = self.display_state
|
||||||
|
@ -384,27 +496,31 @@ class LinkedSplits(QWidget):
|
||||||
prop: Optional[float] = None,
|
prop: Optional[float] = None,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''Set the proportion of space allocated for linked subcharts.
|
'''
|
||||||
|
Set the proportion of space allocated for linked subcharts.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
ln = len(self.subplots)
|
ln = len(self.subplots) or 1
|
||||||
|
|
||||||
# proportion allocated to consumer subcharts
|
# proportion allocated to consumer subcharts
|
||||||
if not prop:
|
if not prop:
|
||||||
prop = 3/8*5/8
|
prop = 3/8
|
||||||
|
|
||||||
# if ln < 2:
|
h = self.height()
|
||||||
# prop = 3/8*5/8
|
histview_h = h * (6/16)
|
||||||
|
h = h - histview_h
|
||||||
# elif ln >= 2:
|
|
||||||
# prop = 3/8
|
|
||||||
|
|
||||||
major = 1 - prop
|
major = 1 - prop
|
||||||
min_h_ind = int((self.height() * prop) / ln)
|
min_h_ind = int((h * prop) / ln)
|
||||||
|
sizes = [
|
||||||
|
int(histview_h),
|
||||||
|
int(h * major),
|
||||||
|
]
|
||||||
|
|
||||||
sizes = [int(self.height() * major)]
|
# give all subcharts the same remaining proportional height
|
||||||
sizes.extend([min_h_ind] * ln)
|
sizes.extend([min_h_ind] * ln)
|
||||||
|
|
||||||
|
if self.godwidget.rt_linked is self:
|
||||||
self.splitter.setSizes(sizes)
|
self.splitter.setSizes(sizes)
|
||||||
|
|
||||||
def focus(self) -> None:
|
def focus(self) -> None:
|
||||||
|
@ -498,10 +614,15 @@ class LinkedSplits(QWidget):
|
||||||
'bottom': xaxis,
|
'bottom': xaxis,
|
||||||
}
|
}
|
||||||
|
|
||||||
qframe = ChartnPane(
|
if sidepane is not False:
|
||||||
|
parent = qframe = ChartnPane(
|
||||||
sidepane=sidepane,
|
sidepane=sidepane,
|
||||||
parent=self.splitter,
|
parent=self.splitter,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
parent = self.splitter
|
||||||
|
qframe = None
|
||||||
|
|
||||||
cpw = ChartPlotWidget(
|
cpw = ChartPlotWidget(
|
||||||
|
|
||||||
# this name will be used to register the primary
|
# this name will be used to register the primary
|
||||||
|
@ -509,7 +630,7 @@ class LinkedSplits(QWidget):
|
||||||
name=name,
|
name=name,
|
||||||
data_key=array_key or name,
|
data_key=array_key or name,
|
||||||
|
|
||||||
parent=qframe,
|
parent=parent,
|
||||||
linkedsplits=self,
|
linkedsplits=self,
|
||||||
axisItems=axes,
|
axisItems=axes,
|
||||||
**cpw_kwargs,
|
**cpw_kwargs,
|
||||||
|
@ -537,6 +658,7 @@ class LinkedSplits(QWidget):
|
||||||
self.xaxis_chart = cpw
|
self.xaxis_chart = cpw
|
||||||
cpw.showAxis('bottom')
|
cpw.showAxis('bottom')
|
||||||
|
|
||||||
|
if qframe is not None:
|
||||||
qframe.chart = cpw
|
qframe.chart = cpw
|
||||||
qframe.hbox.addWidget(cpw)
|
qframe.hbox.addWidget(cpw)
|
||||||
|
|
||||||
|
@ -546,13 +668,15 @@ class LinkedSplits(QWidget):
|
||||||
assert cpw.parent() == qframe
|
assert cpw.parent() == qframe
|
||||||
|
|
||||||
# add sidepane **after** chart; place it on axis side
|
# add sidepane **after** chart; place it on axis side
|
||||||
qframe.hbox.addWidget(
|
qframe.set_sidepane(sidepane)
|
||||||
sidepane,
|
# qframe.hbox.addWidget(
|
||||||
alignment=Qt.AlignTop
|
# sidepane,
|
||||||
)
|
# alignment=Qt.AlignTop
|
||||||
|
# )
|
||||||
|
|
||||||
cpw.sidepane = sidepane
|
cpw.sidepane = sidepane
|
||||||
|
|
||||||
cpw.plotItem.vb.linkedsplits = self
|
cpw.plotItem.vb.linked = self
|
||||||
cpw.setFrameStyle(
|
cpw.setFrameStyle(
|
||||||
QtWidgets.QFrame.StyledPanel
|
QtWidgets.QFrame.StyledPanel
|
||||||
# | QtWidgets.QFrame.Plain
|
# | QtWidgets.QFrame.Plain
|
||||||
|
@ -613,9 +737,8 @@ class LinkedSplits(QWidget):
|
||||||
if not _is_main:
|
if not _is_main:
|
||||||
# track by name
|
# track by name
|
||||||
self.subplots[name] = cpw
|
self.subplots[name] = cpw
|
||||||
|
if qframe is not None:
|
||||||
self.splitter.addWidget(qframe)
|
self.splitter.addWidget(qframe)
|
||||||
# scale split regions
|
|
||||||
self.set_split_sizes()
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
assert style == 'bar', 'main chart must be OHLC'
|
assert style == 'bar', 'main chart must be OHLC'
|
||||||
|
@ -641,19 +764,28 @@ class LinkedSplits(QWidget):
|
||||||
|
|
||||||
def resize_sidepanes(
|
def resize_sidepanes(
|
||||||
self,
|
self,
|
||||||
|
from_linked: Optional[LinkedSplits] = None,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
Size all sidepanes based on the OHLC "main" plot and its
|
Size all sidepanes based on the OHLC "main" plot and its
|
||||||
sidepane width.
|
sidepane width.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
if from_linked:
|
||||||
|
main_chart = from_linked.chart
|
||||||
|
else:
|
||||||
main_chart = self.chart
|
main_chart = self.chart
|
||||||
if main_chart:
|
|
||||||
|
if main_chart and main_chart.sidepane:
|
||||||
sp_w = main_chart.sidepane.width()
|
sp_w = main_chart.sidepane.width()
|
||||||
for name, cpw in self.subplots.items():
|
for name, cpw in self.subplots.items():
|
||||||
cpw.sidepane.setMinimumWidth(sp_w)
|
cpw.sidepane.setMinimumWidth(sp_w)
|
||||||
cpw.sidepane.setMaximumWidth(sp_w)
|
cpw.sidepane.setMaximumWidth(sp_w)
|
||||||
|
|
||||||
|
if from_linked:
|
||||||
|
self.chart.sidepane.setMinimumWidth(sp_w)
|
||||||
|
|
||||||
|
|
||||||
class ChartPlotWidget(pg.PlotWidget):
|
class ChartPlotWidget(pg.PlotWidget):
|
||||||
'''
|
'''
|
||||||
|
@ -711,6 +843,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
# NOTE: must be set bfore calling ``.mk_vb()``
|
# NOTE: must be set bfore calling ``.mk_vb()``
|
||||||
self.linked = linkedsplits
|
self.linked = linkedsplits
|
||||||
|
self.sidepane: Optional[FieldsForm] = None
|
||||||
|
|
||||||
# source of our custom interactions
|
# source of our custom interactions
|
||||||
self.cv = cv = self.mk_vb(name)
|
self.cv = cv = self.mk_vb(name)
|
||||||
|
@ -867,7 +1000,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
def default_view(
|
def default_view(
|
||||||
self,
|
self,
|
||||||
bars_from_y: int = 616,
|
bars_from_y: int = int(616 * 3/8),
|
||||||
|
y_offset: int = 0,
|
||||||
do_ds: bool = True,
|
do_ds: bool = True,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -906,8 +1040,12 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# terms now that we've scaled either by user control
|
# terms now that we've scaled either by user control
|
||||||
# or to the default set of bars as per the immediate block
|
# or to the default set of bars as per the immediate block
|
||||||
# above.
|
# above.
|
||||||
|
if not y_offset:
|
||||||
marker_pos, l1_len = self.pre_l1_xs()
|
marker_pos, l1_len = self.pre_l1_xs()
|
||||||
end = xlast + l1_len + 1
|
end = xlast + l1_len + 1
|
||||||
|
else:
|
||||||
|
end = xlast + y_offset + 1
|
||||||
|
|
||||||
begin = end - (r - l)
|
begin = end - (r - l)
|
||||||
|
|
||||||
# for debugging
|
# for debugging
|
||||||
|
|
|
@ -18,8 +18,13 @@
|
||||||
Mouse interaction graphics
|
Mouse interaction graphics
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import Optional, Callable
|
from typing import (
|
||||||
|
Optional,
|
||||||
|
Callable,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
)
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
@ -36,6 +41,12 @@ from ._style import (
|
||||||
from ._axes import YAxisLabel, XAxisLabel
|
from ._axes import YAxisLabel, XAxisLabel
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ._chart import (
|
||||||
|
ChartPlotWidget,
|
||||||
|
LinkedSplits,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
@ -58,7 +69,7 @@ class LineDot(pg.CurvePoint):
|
||||||
curve: pg.PlotCurveItem,
|
curve: pg.PlotCurveItem,
|
||||||
index: int,
|
index: int,
|
||||||
|
|
||||||
plot: 'ChartPlotWidget', # type: ingore # noqa
|
plot: ChartPlotWidget, # type: ingore # noqa
|
||||||
pos=None,
|
pos=None,
|
||||||
color: str = 'default_light',
|
color: str = 'default_light',
|
||||||
|
|
||||||
|
@ -151,7 +162,7 @@ class ContentsLabel(pg.LabelItem):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|
||||||
# chart: 'ChartPlotWidget', # noqa
|
# chart: ChartPlotWidget, # noqa
|
||||||
view: pg.ViewBox,
|
view: pg.ViewBox,
|
||||||
|
|
||||||
anchor_at: str = ('top', 'right'),
|
anchor_at: str = ('top', 'right'),
|
||||||
|
@ -244,7 +255,7 @@ class ContentsLabels:
|
||||||
'''
|
'''
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
linkedsplits: 'LinkedSplits', # type: ignore # noqa
|
linkedsplits: LinkedSplits, # type: ignore # noqa
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
|
@ -289,7 +300,7 @@ class ContentsLabels:
|
||||||
def add_label(
|
def add_label(
|
||||||
|
|
||||||
self,
|
self,
|
||||||
chart: 'ChartPlotWidget', # type: ignore # noqa
|
chart: ChartPlotWidget, # type: ignore # noqa
|
||||||
name: str,
|
name: str,
|
||||||
anchor_at: tuple[str, str] = ('top', 'left'),
|
anchor_at: tuple[str, str] = ('top', 'left'),
|
||||||
update_func: Callable = ContentsLabel.update_from_value,
|
update_func: Callable = ContentsLabel.update_from_value,
|
||||||
|
@ -316,7 +327,7 @@ class Cursor(pg.GraphicsObject):
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
||||||
self,
|
self,
|
||||||
linkedsplits: 'LinkedSplits', # noqa
|
linkedsplits: LinkedSplits, # noqa
|
||||||
digits: int = 0
|
digits: int = 0
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -325,6 +336,8 @@ class Cursor(pg.GraphicsObject):
|
||||||
|
|
||||||
self.linked = linkedsplits
|
self.linked = linkedsplits
|
||||||
self.graphics: dict[str, pg.GraphicsObject] = {}
|
self.graphics: dict[str, pg.GraphicsObject] = {}
|
||||||
|
self.xaxis_label: Optional[XAxisLabel] = None
|
||||||
|
self.always_show_xlabel: bool = True
|
||||||
self.plots: list['PlotChartWidget'] = [] # type: ignore # noqa
|
self.plots: list['PlotChartWidget'] = [] # type: ignore # noqa
|
||||||
self.active_plot = None
|
self.active_plot = None
|
||||||
self.digits: int = digits
|
self.digits: int = digits
|
||||||
|
@ -385,7 +398,7 @@ class Cursor(pg.GraphicsObject):
|
||||||
|
|
||||||
def add_plot(
|
def add_plot(
|
||||||
self,
|
self,
|
||||||
plot: 'ChartPlotWidget', # noqa
|
plot: ChartPlotWidget, # noqa
|
||||||
digits: int = 0,
|
digits: int = 0,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -469,7 +482,7 @@ class Cursor(pg.GraphicsObject):
|
||||||
|
|
||||||
def add_curve_cursor(
|
def add_curve_cursor(
|
||||||
self,
|
self,
|
||||||
plot: 'ChartPlotWidget', # noqa
|
plot: ChartPlotWidget, # noqa
|
||||||
curve: 'PlotCurveItem', # noqa
|
curve: 'PlotCurveItem', # noqa
|
||||||
|
|
||||||
) -> LineDot:
|
) -> LineDot:
|
||||||
|
@ -491,17 +504,29 @@ class Cursor(pg.GraphicsObject):
|
||||||
log.debug(f"{(action, plot.name)}")
|
log.debug(f"{(action, plot.name)}")
|
||||||
if action == 'Enter':
|
if action == 'Enter':
|
||||||
self.active_plot = plot
|
self.active_plot = plot
|
||||||
|
plot.linked.godwidget._active_cursor = self
|
||||||
|
|
||||||
# show horiz line and y-label
|
# show horiz line and y-label
|
||||||
self.graphics[plot]['hl'].show()
|
self.graphics[plot]['hl'].show()
|
||||||
self.graphics[plot]['yl'].show()
|
self.graphics[plot]['yl'].show()
|
||||||
|
|
||||||
else: # Leave
|
if (
|
||||||
|
not self.always_show_xlabel
|
||||||
|
and not self.xaxis_label.isVisible()
|
||||||
|
):
|
||||||
|
self.xaxis_label.show()
|
||||||
|
|
||||||
# hide horiz line and y-label
|
# Leave: hide horiz line and y-label
|
||||||
|
else:
|
||||||
self.graphics[plot]['hl'].hide()
|
self.graphics[plot]['hl'].hide()
|
||||||
self.graphics[plot]['yl'].hide()
|
self.graphics[plot]['yl'].hide()
|
||||||
|
|
||||||
|
if (
|
||||||
|
not self.always_show_xlabel
|
||||||
|
and self.xaxis_label.isVisible()
|
||||||
|
):
|
||||||
|
self.xaxis_label.hide()
|
||||||
|
|
||||||
def mouseMoved(
|
def mouseMoved(
|
||||||
self,
|
self,
|
||||||
coords: tuple[QPointF], # noqa
|
coords: tuple[QPointF], # noqa
|
||||||
|
@ -590,6 +615,10 @@ class Cursor(pg.GraphicsObject):
|
||||||
left_axis_width += left.width()
|
left_axis_width += left.width()
|
||||||
|
|
||||||
# map back to abs (label-local) coordinates
|
# map back to abs (label-local) coordinates
|
||||||
|
if (
|
||||||
|
self.always_show_xlabel
|
||||||
|
or self.xaxis_label.isVisible()
|
||||||
|
):
|
||||||
self.xaxis_label.update_label(
|
self.xaxis_label.update_label(
|
||||||
abs_pos=(
|
abs_pos=(
|
||||||
plot.mapFromView(QPointF(vl_x, iy)) -
|
plot.mapFromView(QPointF(vl_x, iy)) -
|
||||||
|
|
|
@ -21,19 +21,20 @@ this module ties together quote and computational (fsp) streams with
|
||||||
graphics update methods via our custom ``pyqtgraph`` charting api.
|
graphics update methods via our custom ``pyqtgraph`` charting api.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from dataclasses import dataclass
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import time
|
import time
|
||||||
from typing import Optional, Any, Callable
|
from typing import Optional, Any, Callable
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import tractor
|
import tractor
|
||||||
import trio
|
import trio
|
||||||
import pendulum
|
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
|
|
||||||
# from .. import brokers
|
# from .. import brokers
|
||||||
from ..data.feed import open_feed
|
from ..data.feed import (
|
||||||
|
open_feed,
|
||||||
|
Feed,
|
||||||
|
)
|
||||||
|
from ..data.types import Struct
|
||||||
from ._axes import YAxisLabel
|
from ._axes import YAxisLabel
|
||||||
from ._chart import (
|
from ._chart import (
|
||||||
ChartPlotWidget,
|
ChartPlotWidget,
|
||||||
|
@ -41,6 +42,7 @@ from ._chart import (
|
||||||
GodWidget,
|
GodWidget,
|
||||||
)
|
)
|
||||||
from ._l1 import L1Labels
|
from ._l1 import L1Labels
|
||||||
|
from ._style import hcolor
|
||||||
from ._fsp import (
|
from ._fsp import (
|
||||||
update_fsp_chart,
|
update_fsp_chart,
|
||||||
start_fsp_displays,
|
start_fsp_displays,
|
||||||
|
@ -53,7 +55,10 @@ from ._forms import (
|
||||||
FieldsForm,
|
FieldsForm,
|
||||||
mk_order_pane_layout,
|
mk_order_pane_layout,
|
||||||
)
|
)
|
||||||
from .order_mode import open_order_mode
|
from .order_mode import (
|
||||||
|
open_order_mode,
|
||||||
|
OrderMode,
|
||||||
|
)
|
||||||
from .._profile import (
|
from .._profile import (
|
||||||
pg_profile_enabled,
|
pg_profile_enabled,
|
||||||
ms_slower_then,
|
ms_slower_then,
|
||||||
|
@ -63,7 +68,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 = 16 # Hz
|
||||||
|
|
||||||
|
|
||||||
# a working tick-type-classes template
|
# a working tick-type-classes template
|
||||||
|
@ -122,39 +127,105 @@ def chart_maxmin(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
class DisplayState(Struct):
|
||||||
class DisplayState:
|
|
||||||
'''
|
'''
|
||||||
Chart-local real-time graphics state container.
|
Chart-local real-time graphics state container.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
godwidget: GodWidget
|
||||||
quotes: dict[str, Any]
|
quotes: dict[str, Any]
|
||||||
|
|
||||||
maxmin: Callable
|
maxmin: Callable
|
||||||
ohlcv: ShmArray
|
ohlcv: ShmArray
|
||||||
|
hist_ohlcv: ShmArray
|
||||||
|
|
||||||
# high level chart handles
|
# high level chart handles
|
||||||
linked: LinkedSplits
|
|
||||||
chart: ChartPlotWidget
|
chart: ChartPlotWidget
|
||||||
|
|
||||||
# axis labels
|
# axis labels
|
||||||
l1: L1Labels
|
l1: L1Labels
|
||||||
last_price_sticky: YAxisLabel
|
last_price_sticky: YAxisLabel
|
||||||
|
hist_last_price_sticky: YAxisLabel
|
||||||
|
|
||||||
# misc state tracking
|
# misc state tracking
|
||||||
vars: dict[str, Any]
|
vars: dict[str, Any] = {
|
||||||
|
'tick_margin': 0,
|
||||||
|
'i_last': 0,
|
||||||
|
'i_last_append': 0,
|
||||||
|
'last_mx_vlm': 0,
|
||||||
|
'last_mx': 0,
|
||||||
|
'last_mn': 0,
|
||||||
|
}
|
||||||
|
|
||||||
vlm_chart: Optional[ChartPlotWidget] = None
|
vlm_chart: Optional[ChartPlotWidget] = None
|
||||||
vlm_sticky: Optional[YAxisLabel] = None
|
vlm_sticky: Optional[YAxisLabel] = None
|
||||||
wap_in_history: bool = False
|
wap_in_history: bool = False
|
||||||
|
|
||||||
|
def incr_info(
|
||||||
|
self,
|
||||||
|
chart: Optional[ChartPlotWidget] = None,
|
||||||
|
shm: Optional[ShmArray] = None,
|
||||||
|
state: Optional[dict] = None, # pass in a copy if you don't
|
||||||
|
|
||||||
|
update_state: bool = True,
|
||||||
|
update_uppx: float = 16,
|
||||||
|
|
||||||
|
) -> tuple:
|
||||||
|
|
||||||
|
shm = shm or self.ohlcv
|
||||||
|
chart = chart or self.chart
|
||||||
|
state = state or self.vars
|
||||||
|
|
||||||
|
if not update_state:
|
||||||
|
state = state.copy()
|
||||||
|
|
||||||
|
# compute the first available graphic's x-units-per-pixel
|
||||||
|
uppx = chart.view.x_uppx()
|
||||||
|
|
||||||
|
# NOTE: this used to be implemented in a dedicated
|
||||||
|
# "increment task": ``check_for_new_bars()`` but it doesn't
|
||||||
|
# make sense to do a whole task switch when we can just do
|
||||||
|
# this simple index-diff and all the fsp sub-curve graphics
|
||||||
|
# are diffed on each draw cycle anyway; so updates to the
|
||||||
|
# "curve" length is already automatic.
|
||||||
|
|
||||||
|
# increment the view position by the sample offset.
|
||||||
|
i_step = shm.index
|
||||||
|
i_diff = i_step - state['i_last']
|
||||||
|
state['i_last'] = i_step
|
||||||
|
|
||||||
|
append_diff = i_step - state['i_last_append']
|
||||||
|
|
||||||
|
# update the "last datum" (aka extending the flow graphic with
|
||||||
|
# new data) only if the number of unit steps is >= the number of
|
||||||
|
# such unit steps per pixel (aka uppx). Iow, if the zoom level
|
||||||
|
# is such that a datum(s) update to graphics wouldn't span
|
||||||
|
# to a new pixel, we don't update yet.
|
||||||
|
do_append = (append_diff >= uppx)
|
||||||
|
if do_append:
|
||||||
|
state['i_last_append'] = i_step
|
||||||
|
|
||||||
|
do_rt_update = uppx < update_uppx
|
||||||
|
|
||||||
|
_, _, _, r = chart.bars_range()
|
||||||
|
liv = r >= i_step
|
||||||
|
|
||||||
|
# TODO: pack this into a struct
|
||||||
|
return (
|
||||||
|
uppx,
|
||||||
|
liv,
|
||||||
|
do_append,
|
||||||
|
i_diff,
|
||||||
|
append_diff,
|
||||||
|
do_rt_update,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def graphics_update_loop(
|
async def graphics_update_loop(
|
||||||
|
|
||||||
linked: LinkedSplits,
|
nurse: trio.Nursery,
|
||||||
stream: tractor.MsgStream,
|
godwidget: GodWidget,
|
||||||
ohlcv: np.ndarray,
|
feed: Feed,
|
||||||
|
|
||||||
wap_in_history: bool = False,
|
wap_in_history: bool = False,
|
||||||
vlm_chart: Optional[ChartPlotWidget] = None,
|
vlm_chart: Optional[ChartPlotWidget] = None,
|
||||||
|
|
||||||
|
@ -175,9 +246,14 @@ async def graphics_update_loop(
|
||||||
# of copying it from last bar's close
|
# of copying it from last bar's close
|
||||||
# - 1-5 sec bar lookback-autocorrection like tws does?
|
# - 1-5 sec bar lookback-autocorrection like tws does?
|
||||||
# (would require a background history checker task)
|
# (would require a background history checker task)
|
||||||
display_rate = linked.godwidget.window.current_screen().refreshRate()
|
linked: LinkedSplits = godwidget.rt_linked
|
||||||
|
display_rate = godwidget.window.current_screen().refreshRate()
|
||||||
|
|
||||||
chart = linked.chart
|
chart = linked.chart
|
||||||
|
hist_chart = godwidget.hist_linked.chart
|
||||||
|
|
||||||
|
ohlcv = feed.rt_shm
|
||||||
|
hist_ohlcv = feed.hist_shm
|
||||||
|
|
||||||
# update last price sticky
|
# update last price sticky
|
||||||
last_price_sticky = chart._ysticks[chart.name]
|
last_price_sticky = chart._ysticks[chart.name]
|
||||||
|
@ -185,6 +261,11 @@ async def graphics_update_loop(
|
||||||
*ohlcv.array[-1][['index', 'close']]
|
*ohlcv.array[-1][['index', 'close']]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
hist_last_price_sticky = hist_chart._ysticks[hist_chart.name]
|
||||||
|
hist_last_price_sticky.update_from_data(
|
||||||
|
*hist_ohlcv.array[-1][['index', 'close']]
|
||||||
|
)
|
||||||
|
|
||||||
maxmin = partial(
|
maxmin = partial(
|
||||||
chart_maxmin,
|
chart_maxmin,
|
||||||
chart,
|
chart,
|
||||||
|
@ -227,12 +308,14 @@ async def graphics_update_loop(
|
||||||
i_last = ohlcv.index
|
i_last = ohlcv.index
|
||||||
|
|
||||||
ds = linked.display_state = DisplayState(**{
|
ds = linked.display_state = DisplayState(**{
|
||||||
|
'godwidget': godwidget,
|
||||||
'quotes': {},
|
'quotes': {},
|
||||||
'linked': linked,
|
|
||||||
'maxmin': maxmin,
|
'maxmin': maxmin,
|
||||||
'ohlcv': ohlcv,
|
'ohlcv': ohlcv,
|
||||||
|
'hist_ohlcv': hist_ohlcv,
|
||||||
'chart': chart,
|
'chart': chart,
|
||||||
'last_price_sticky': last_price_sticky,
|
'last_price_sticky': last_price_sticky,
|
||||||
|
'hist_last_price_sticky': hist_last_price_sticky,
|
||||||
'l1': l1,
|
'l1': l1,
|
||||||
|
|
||||||
'vars': {
|
'vars': {
|
||||||
|
@ -252,7 +335,62 @@ async def graphics_update_loop(
|
||||||
|
|
||||||
chart.default_view()
|
chart.default_view()
|
||||||
|
|
||||||
|
# TODO: probably factor this into some kinda `DisplayState`
|
||||||
|
# API that can be reused at least in terms of pulling view
|
||||||
|
# params (eg ``.bars_range()``).
|
||||||
|
async def increment_history_view():
|
||||||
|
i_last = hist_ohlcv.index
|
||||||
|
state = ds.vars.copy() | {
|
||||||
|
'i_last_append': i_last,
|
||||||
|
'i_last': i_last,
|
||||||
|
}
|
||||||
|
_, hist_step_size_s, _ = feed.get_ds_info()
|
||||||
|
|
||||||
|
async with feed.index_stream(
|
||||||
|
# int(hist_step_size_s)
|
||||||
|
# TODO: seems this is more reliable at keeping the slow
|
||||||
|
# chart incremented in view more correctly?
|
||||||
|
# - It might make sense to just inline this logic with the
|
||||||
|
# main display task? => it's a tradeoff of slower task
|
||||||
|
# wakeups/ctx switches verus logic checks (as normal)
|
||||||
|
# - we need increment logic that only does the view shift
|
||||||
|
# call when the uppx permits/needs it
|
||||||
|
int(1),
|
||||||
|
) as istream:
|
||||||
|
async for msg in istream:
|
||||||
|
|
||||||
|
# check if slow chart needs an x-domain shift and/or
|
||||||
|
# y-range resize.
|
||||||
|
(
|
||||||
|
uppx,
|
||||||
|
liv,
|
||||||
|
do_append,
|
||||||
|
i_diff,
|
||||||
|
append_diff,
|
||||||
|
do_rt_update,
|
||||||
|
) = ds.incr_info(
|
||||||
|
chart=hist_chart,
|
||||||
|
shm=ds.hist_ohlcv,
|
||||||
|
state=state,
|
||||||
|
# update_state=False,
|
||||||
|
)
|
||||||
|
# print(
|
||||||
|
# f'liv: {liv}\n'
|
||||||
|
# f'do_append: {do_append}\n'
|
||||||
|
# f'append_diff: {append_diff}\n'
|
||||||
|
# )
|
||||||
|
|
||||||
|
if (
|
||||||
|
do_append
|
||||||
|
and liv
|
||||||
|
):
|
||||||
|
hist_chart.increment_view(steps=i_diff)
|
||||||
|
hist_chart.view._set_yrange(yrange=hist_chart.maxmin())
|
||||||
|
|
||||||
|
nurse.start_soon(increment_history_view)
|
||||||
|
|
||||||
# main real-time quotes update loop
|
# main real-time quotes update loop
|
||||||
|
stream: tractor.MsgStream = feed.stream
|
||||||
async for quotes in stream:
|
async for quotes in stream:
|
||||||
|
|
||||||
ds.quotes = quotes
|
ds.quotes = quotes
|
||||||
|
@ -273,7 +411,7 @@ async def graphics_update_loop(
|
||||||
|
|
||||||
# chart isn't active/shown so skip render cycle and pause feed(s)
|
# chart isn't active/shown so skip render cycle and pause feed(s)
|
||||||
if chart.linked.isHidden():
|
if chart.linked.isHidden():
|
||||||
print('skipping update')
|
# print('skipping update')
|
||||||
chart.pause_all_feeds()
|
chart.pause_all_feeds()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -298,6 +436,8 @@ def graphics_update_cycle(
|
||||||
# hopefully XD
|
# hopefully XD
|
||||||
|
|
||||||
chart = ds.chart
|
chart = ds.chart
|
||||||
|
# TODO: just pass this as a direct ref to avoid so many attr accesses?
|
||||||
|
hist_chart = ds.godwidget.hist_linked.chart
|
||||||
|
|
||||||
profiler = pg.debug.Profiler(
|
profiler = pg.debug.Profiler(
|
||||||
msg=f'Graphics loop cycle for: `{chart.name}`',
|
msg=f'Graphics loop cycle for: `{chart.name}`',
|
||||||
|
@ -311,53 +451,24 @@ def graphics_update_cycle(
|
||||||
|
|
||||||
# unpack multi-referenced components
|
# unpack multi-referenced components
|
||||||
vlm_chart = ds.vlm_chart
|
vlm_chart = ds.vlm_chart
|
||||||
|
|
||||||
|
# rt "HFT" chart
|
||||||
l1 = ds.l1
|
l1 = ds.l1
|
||||||
ohlcv = ds.ohlcv
|
ohlcv = ds.ohlcv
|
||||||
array = ohlcv.array
|
array = ohlcv.array
|
||||||
|
|
||||||
vars = ds.vars
|
vars = ds.vars
|
||||||
tick_margin = vars['tick_margin']
|
tick_margin = vars['tick_margin']
|
||||||
|
|
||||||
update_uppx = 16
|
|
||||||
|
|
||||||
for sym, quote in ds.quotes.items():
|
for sym, quote in ds.quotes.items():
|
||||||
|
(
|
||||||
# compute the first available graphic's x-units-per-pixel
|
uppx,
|
||||||
uppx = chart.view.x_uppx()
|
liv,
|
||||||
|
do_append,
|
||||||
# NOTE: vlm may be written by the ``brokerd`` backend
|
i_diff,
|
||||||
# event though a tick sample is not emitted.
|
append_diff,
|
||||||
# TODO: show dark trades differently
|
do_rt_update,
|
||||||
# https://github.com/pikers/piker/issues/116
|
) = ds.incr_info()
|
||||||
|
|
||||||
# NOTE: this used to be implemented in a dedicated
|
|
||||||
# "increment task": ``check_for_new_bars()`` but it doesn't
|
|
||||||
# make sense to do a whole task switch when we can just do
|
|
||||||
# this simple index-diff and all the fsp sub-curve graphics
|
|
||||||
# are diffed on each draw cycle anyway; so updates to the
|
|
||||||
# "curve" length is already automatic.
|
|
||||||
|
|
||||||
# increment the view position by the sample offset.
|
|
||||||
i_step = ohlcv.index
|
|
||||||
i_diff = i_step - vars['i_last']
|
|
||||||
vars['i_last'] = i_step
|
|
||||||
|
|
||||||
append_diff = i_step - vars['i_last_append']
|
|
||||||
|
|
||||||
# update the "last datum" (aka extending the flow graphic with
|
|
||||||
# new data) only if the number of unit steps is >= the number of
|
|
||||||
# such unit steps per pixel (aka uppx). Iow, if the zoom level
|
|
||||||
# is such that a datum(s) update to graphics wouldn't span
|
|
||||||
# to a new pixel, we don't update yet.
|
|
||||||
do_append = (append_diff >= uppx)
|
|
||||||
if do_append:
|
|
||||||
vars['i_last_append'] = i_step
|
|
||||||
|
|
||||||
do_rt_update = uppx < update_uppx
|
|
||||||
# print(
|
|
||||||
# f'append_diff:{append_diff}\n'
|
|
||||||
# f'uppx:{uppx}\n'
|
|
||||||
# f'do_append: {do_append}'
|
|
||||||
# )
|
|
||||||
|
|
||||||
# TODO: we should only run mxmn when we know
|
# TODO: we should only run mxmn when we know
|
||||||
# an update is due via ``do_append`` above.
|
# an update is due via ``do_append`` above.
|
||||||
|
@ -373,8 +484,6 @@ def graphics_update_cycle(
|
||||||
|
|
||||||
profiler('`ds.maxmin()` call')
|
profiler('`ds.maxmin()` call')
|
||||||
|
|
||||||
liv = r >= i_step # the last datum is in view
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
prepend_update_index is not None
|
prepend_update_index is not None
|
||||||
and lbar > prepend_update_index
|
and lbar > prepend_update_index
|
||||||
|
@ -389,16 +498,10 @@ def graphics_update_cycle(
|
||||||
# don't real-time "shift" the curve to the
|
# don't real-time "shift" the curve to the
|
||||||
# left unless we get one of the following:
|
# left unless we get one of the following:
|
||||||
if (
|
if (
|
||||||
(
|
(do_append and liv)
|
||||||
# i_diff > 0 # no new sample step
|
|
||||||
do_append
|
|
||||||
# and uppx < 4 # chart is zoomed out very far
|
|
||||||
and liv
|
|
||||||
)
|
|
||||||
or trigger_all
|
or trigger_all
|
||||||
):
|
):
|
||||||
chart.increment_view(steps=i_diff)
|
chart.increment_view(steps=i_diff)
|
||||||
# chart.increment_view(steps=i_diff + round(append_diff - uppx))
|
|
||||||
|
|
||||||
if vlm_chart:
|
if vlm_chart:
|
||||||
vlm_chart.increment_view(steps=i_diff)
|
vlm_chart.increment_view(steps=i_diff)
|
||||||
|
@ -458,6 +561,10 @@ def graphics_update_cycle(
|
||||||
chart.name,
|
chart.name,
|
||||||
do_append=do_append,
|
do_append=do_append,
|
||||||
)
|
)
|
||||||
|
hist_chart.update_graphics_from_flow(
|
||||||
|
chart.name,
|
||||||
|
do_append=do_append,
|
||||||
|
)
|
||||||
|
|
||||||
# NOTE: we always update the "last" datum
|
# NOTE: we always update the "last" datum
|
||||||
# since the current range should at least be updated
|
# since the current range should at least be updated
|
||||||
|
@ -495,6 +602,9 @@ def graphics_update_cycle(
|
||||||
ds.last_price_sticky.update_from_data(
|
ds.last_price_sticky.update_from_data(
|
||||||
*end[['index', 'close']]
|
*end[['index', 'close']]
|
||||||
)
|
)
|
||||||
|
ds.hist_last_price_sticky.update_from_data(
|
||||||
|
*end[['index', 'close']]
|
||||||
|
)
|
||||||
|
|
||||||
if wap_in_history:
|
if wap_in_history:
|
||||||
# update vwap overlay line
|
# update vwap overlay line
|
||||||
|
@ -542,10 +652,12 @@ def graphics_update_cycle(
|
||||||
l1.bid_label.update_fields({'level': price, 'size': size})
|
l1.bid_label.update_fields({'level': price, 'size': size})
|
||||||
|
|
||||||
# check for y-range re-size
|
# check for y-range re-size
|
||||||
|
if (mx > vars['last_mx']) or (mn < vars['last_mn']):
|
||||||
|
|
||||||
|
# fast chart resize case
|
||||||
if (
|
if (
|
||||||
(mx > vars['last_mx']) or (mn < vars['last_mn'])
|
liv
|
||||||
and not chart._static_yrange == 'axis'
|
and not chart._static_yrange == 'axis'
|
||||||
and liv
|
|
||||||
):
|
):
|
||||||
main_vb = chart.view
|
main_vb = chart.view
|
||||||
if (
|
if (
|
||||||
|
@ -563,6 +675,22 @@ def graphics_update_cycle(
|
||||||
yrange=(mn, mx),
|
yrange=(mn, mx),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# check if slow chart needs a resize
|
||||||
|
(
|
||||||
|
_,
|
||||||
|
hist_liv,
|
||||||
|
_,
|
||||||
|
_,
|
||||||
|
_,
|
||||||
|
_,
|
||||||
|
) = ds.incr_info(
|
||||||
|
chart=hist_chart,
|
||||||
|
shm=ds.hist_ohlcv,
|
||||||
|
update_state=False,
|
||||||
|
)
|
||||||
|
if hist_liv:
|
||||||
|
hist_chart.view._set_yrange(yrange=hist_chart.maxmin())
|
||||||
|
|
||||||
# XXX: update this every draw cycle to make L1-always-in-view work.
|
# XXX: update this every draw cycle to make L1-always-in-view work.
|
||||||
vars['last_mx'], vars['last_mn'] = mx, mn
|
vars['last_mx'], vars['last_mn'] = mx, mn
|
||||||
|
|
||||||
|
@ -719,15 +847,17 @@ async def display_symbol_data(
|
||||||
tick_throttle=_quote_throttle_rate,
|
tick_throttle=_quote_throttle_rate,
|
||||||
|
|
||||||
) as feed:
|
) as feed:
|
||||||
ohlcv: ShmArray = feed.shm
|
ohlcv: ShmArray = feed.rt_shm
|
||||||
bars = ohlcv.array
|
hist_ohlcv: ShmArray = feed.hist_shm
|
||||||
|
|
||||||
|
# this value needs to be pulled once and only once during
|
||||||
|
# startup
|
||||||
|
end_index = feed.startup_hist_index
|
||||||
|
|
||||||
symbol = feed.symbols[sym]
|
symbol = feed.symbols[sym]
|
||||||
fqsn = symbol.front_fqsn()
|
fqsn = symbol.front_fqsn()
|
||||||
|
|
||||||
times = bars['time']
|
step_size_s = 1
|
||||||
end = pendulum.from_timestamp(times[-1])
|
|
||||||
start = pendulum.from_timestamp(times[times != times[-1]][-1])
|
|
||||||
step_size_s = (end - start).seconds
|
|
||||||
tf_key = tf_in_1s[step_size_s]
|
tf_key = tf_in_1s[step_size_s]
|
||||||
|
|
||||||
# load in symbol's ohlc data
|
# load in symbol's ohlc data
|
||||||
|
@ -737,33 +867,48 @@ async def display_symbol_data(
|
||||||
f'step:{tf_key} '
|
f'step:{tf_key} '
|
||||||
)
|
)
|
||||||
|
|
||||||
linked = godwidget.linkedsplits
|
rt_linked = godwidget.rt_linked
|
||||||
linked._symbol = symbol
|
rt_linked._symbol = symbol
|
||||||
|
|
||||||
|
# create top history view chart above the "main rt chart".
|
||||||
|
hist_linked = godwidget.hist_linked
|
||||||
|
hist_linked._symbol = symbol
|
||||||
|
hist_chart = hist_linked.plot_ohlc_main(
|
||||||
|
symbol,
|
||||||
|
feed.hist_shm,
|
||||||
|
# in the case of history chart we explicitly set `False`
|
||||||
|
# to avoid internal pane creation.
|
||||||
|
# sidepane=False,
|
||||||
|
sidepane=godwidget.search,
|
||||||
|
)
|
||||||
|
# don't show when not focussed
|
||||||
|
hist_linked.cursor.always_show_xlabel = False
|
||||||
|
|
||||||
# generate order mode side-pane UI
|
# generate order mode side-pane UI
|
||||||
# A ``FieldsForm`` form to configure order entry
|
# A ``FieldsForm`` form to configure order entry
|
||||||
|
# and add as next-to-y-axis singleton pane
|
||||||
pp_pane: FieldsForm = mk_order_pane_layout(godwidget)
|
pp_pane: FieldsForm = mk_order_pane_layout(godwidget)
|
||||||
|
|
||||||
# add as next-to-y-axis singleton pane
|
|
||||||
godwidget.pp_pane = pp_pane
|
godwidget.pp_pane = pp_pane
|
||||||
|
|
||||||
# create main OHLC chart
|
# create main OHLC chart
|
||||||
chart = linked.plot_ohlc_main(
|
chart = rt_linked.plot_ohlc_main(
|
||||||
symbol,
|
symbol,
|
||||||
ohlcv,
|
ohlcv,
|
||||||
|
# in the case of history chart we explicitly set `False`
|
||||||
|
# to avoid internal pane creation.
|
||||||
sidepane=pp_pane,
|
sidepane=pp_pane,
|
||||||
)
|
)
|
||||||
chart.default_view()
|
|
||||||
chart._feeds[symbol.key] = feed
|
chart._feeds[symbol.key] = feed
|
||||||
chart.setFocus()
|
chart.setFocus()
|
||||||
|
|
||||||
|
# XXX: FOR SOME REASON THIS IS CAUSING HANGZ!?!
|
||||||
# plot historical vwap if available
|
# plot historical vwap if available
|
||||||
wap_in_history = False
|
wap_in_history = False
|
||||||
|
# if (
|
||||||
# XXX: FOR SOME REASON THIS IS CAUSING HANGZ!?!
|
# brokermod._show_wap_in_history
|
||||||
# if brokermod._show_wap_in_history:
|
# and 'bar_wap' in bars.dtype.fields
|
||||||
|
# ):
|
||||||
# if 'bar_wap' in bars.dtype.fields:
|
|
||||||
# wap_in_history = True
|
# wap_in_history = True
|
||||||
# chart.draw_curve(
|
# chart.draw_curve(
|
||||||
# name='bar_wap',
|
# name='bar_wap',
|
||||||
|
@ -772,16 +917,108 @@ async def display_symbol_data(
|
||||||
# add_label=False,
|
# add_label=False,
|
||||||
# )
|
# )
|
||||||
|
|
||||||
# size view to data once at outset
|
# Add the LinearRegionItem to the ViewBox, but tell the ViewBox
|
||||||
chart.cv._set_yrange()
|
# to exclude this item when doing auto-range calculations.
|
||||||
|
rt_pi = chart.plotItem
|
||||||
|
hist_pi = hist_chart.plotItem
|
||||||
|
region = pg.LinearRegionItem(
|
||||||
|
# color scheme that matches sidepane styling
|
||||||
|
pen=pg.mkPen(hcolor('gunmetal')),
|
||||||
|
brush=pg.mkBrush(hcolor('default_darkest')),
|
||||||
|
)
|
||||||
|
region.setZValue(10) # put linear region "in front" in layer terms
|
||||||
|
hist_pi.addItem(region, ignoreBounds=True)
|
||||||
|
flow = chart._flows[hist_chart.name]
|
||||||
|
assert flow
|
||||||
|
# XXX: no idea why this doesn't work but it's causing
|
||||||
|
# a weird placement of the region on the way-far-left..
|
||||||
|
# region.setClipItem(flow.graphics)
|
||||||
|
|
||||||
|
# poll for datums load and timestep detection
|
||||||
|
for _ in range(100):
|
||||||
|
try:
|
||||||
|
_, _, ratio = feed.get_ds_info()
|
||||||
|
break
|
||||||
|
except IndexError:
|
||||||
|
await trio.sleep(0.01)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
'Failed to detect sampling periods from shm!?')
|
||||||
|
|
||||||
|
def update_pi_from_region():
|
||||||
|
region.setZValue(10)
|
||||||
|
mn, mx = region.getRegion()
|
||||||
|
# print(f'region_x: {(mn, mx)}')
|
||||||
|
|
||||||
|
# XXX: seems to cause a real perf hit?
|
||||||
|
rt_pi.setXRange(
|
||||||
|
(mn - end_index) * ratio,
|
||||||
|
(mx - end_index) * ratio,
|
||||||
|
padding=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
region.sigRegionChanged.connect(update_pi_from_region)
|
||||||
|
|
||||||
|
def update_region_from_pi(
|
||||||
|
window,
|
||||||
|
viewRange: tuple[tuple, tuple],
|
||||||
|
is_manual: bool = True,
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
# set the region on the history chart
|
||||||
|
# to the range currently viewed in the
|
||||||
|
# HFT/real-time chart.
|
||||||
|
mn, mx = viewRange[0]
|
||||||
|
ds_mn = mn/ratio
|
||||||
|
ds_mx = mx/ratio
|
||||||
|
# print(
|
||||||
|
# f'rt_view_range: {(mn, mx)}\n'
|
||||||
|
# f'ds_mn, ds_mx: {(ds_mn, ds_mx)}\n'
|
||||||
|
# )
|
||||||
|
lhmn = ds_mn + end_index
|
||||||
|
lhmx = ds_mx + end_index
|
||||||
|
region.setRegion((
|
||||||
|
lhmn,
|
||||||
|
lhmx,
|
||||||
|
))
|
||||||
|
|
||||||
|
# TODO: if we want to have the slow chart adjust range to
|
||||||
|
# match the fast chart's selection -> results in the
|
||||||
|
# linear region expansion never can go "outside of view".
|
||||||
|
# hmn, hmx = hvr = hist_chart.view.state['viewRange'][0]
|
||||||
|
# print((hmn, hmx))
|
||||||
|
# if (
|
||||||
|
# hvr
|
||||||
|
# and (lhmn < hmn or lhmx > hmx)
|
||||||
|
# ):
|
||||||
|
# hist_pi.setXRange(
|
||||||
|
# lhmn,
|
||||||
|
# lhmx,
|
||||||
|
# padding=0,
|
||||||
|
# )
|
||||||
|
# hist_linked.graphics_cycle()
|
||||||
|
|
||||||
|
# connect region to be updated on plotitem interaction.
|
||||||
|
rt_pi.sigRangeChanged.connect(update_region_from_pi)
|
||||||
|
|
||||||
# NOTE: we must immediately tell Qt to show the OHLC chart
|
# NOTE: we must immediately tell Qt to show the OHLC chart
|
||||||
# to avoid a race where the subplots get added/shown to
|
# to avoid a race where the subplots get added/shown to
|
||||||
# the linked set *before* the main price chart!
|
# the linked set *before* the main price chart!
|
||||||
linked.show()
|
rt_linked.show()
|
||||||
linked.focus()
|
rt_linked.focus()
|
||||||
await trio.sleep(0)
|
await trio.sleep(0)
|
||||||
|
|
||||||
|
# NOTE: here we insert the slow-history chart set into
|
||||||
|
# the fast chart's splitter -> so it's a splitter of charts
|
||||||
|
# inside the first widget slot of a splitter of charts XD
|
||||||
|
rt_linked.splitter.insertWidget(0, hist_linked)
|
||||||
|
# XXX: if we wanted it at the bottom?
|
||||||
|
# rt_linked.splitter.addWidget(hist_linked)
|
||||||
|
rt_linked.focus()
|
||||||
|
|
||||||
|
godwidget.resize_all()
|
||||||
|
|
||||||
vlm_chart: Optional[ChartPlotWidget] = None
|
vlm_chart: Optional[ChartPlotWidget] = None
|
||||||
async with trio.open_nursery() as ln:
|
async with trio.open_nursery() as ln:
|
||||||
|
|
||||||
|
@ -792,7 +1029,7 @@ async def display_symbol_data(
|
||||||
):
|
):
|
||||||
vlm_chart = await ln.start(
|
vlm_chart = await ln.start(
|
||||||
open_vlm_displays,
|
open_vlm_displays,
|
||||||
linked,
|
rt_linked,
|
||||||
ohlcv,
|
ohlcv,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -800,7 +1037,7 @@ async def display_symbol_data(
|
||||||
# from an input config.
|
# from an input config.
|
||||||
ln.start_soon(
|
ln.start_soon(
|
||||||
start_fsp_displays,
|
start_fsp_displays,
|
||||||
linked,
|
rt_linked,
|
||||||
ohlcv,
|
ohlcv,
|
||||||
loading_sym_key,
|
loading_sym_key,
|
||||||
loglevel,
|
loglevel,
|
||||||
|
@ -809,39 +1046,73 @@ async def display_symbol_data(
|
||||||
# start graphics update loop after receiving first live quote
|
# start graphics update loop after receiving first live quote
|
||||||
ln.start_soon(
|
ln.start_soon(
|
||||||
graphics_update_loop,
|
graphics_update_loop,
|
||||||
linked,
|
ln,
|
||||||
feed.stream,
|
godwidget,
|
||||||
ohlcv,
|
feed,
|
||||||
wap_in_history,
|
wap_in_history,
|
||||||
vlm_chart,
|
vlm_chart,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await trio.sleep(0)
|
||||||
|
|
||||||
|
# size view to data prior to order mode init
|
||||||
|
chart.default_view()
|
||||||
|
rt_linked.graphics_cycle()
|
||||||
|
await trio.sleep(0)
|
||||||
|
|
||||||
|
hist_chart.default_view(
|
||||||
|
bars_from_y=int(len(hist_ohlcv.array)), # size to data
|
||||||
|
y_offset=6116*2, # push it a little away from the y-axis
|
||||||
|
)
|
||||||
|
hist_linked.graphics_cycle()
|
||||||
|
await trio.sleep(0)
|
||||||
|
|
||||||
|
godwidget.resize_all()
|
||||||
|
|
||||||
|
mode: OrderMode
|
||||||
async with (
|
async with (
|
||||||
open_order_mode(
|
open_order_mode(
|
||||||
feed,
|
feed,
|
||||||
chart,
|
godwidget,
|
||||||
fqsn,
|
fqsn,
|
||||||
order_mode_started
|
order_mode_started
|
||||||
)
|
) as mode
|
||||||
):
|
):
|
||||||
if not vlm_chart:
|
if not vlm_chart:
|
||||||
|
# trigger another view reset if no sub-chart
|
||||||
chart.default_view()
|
chart.default_view()
|
||||||
|
|
||||||
|
rt_linked.mode = mode
|
||||||
|
|
||||||
# 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)
|
||||||
linked.resize_sidepanes()
|
|
||||||
|
|
||||||
|
# dynamic resize steps
|
||||||
|
godwidget.resize_all()
|
||||||
|
|
||||||
|
# TODO: look into this because not sure why it was
|
||||||
|
# commented out / we ever needed it XD
|
||||||
# NOTE: we pop the volume chart from the subplots set so
|
# NOTE: we pop the volume chart from the subplots set so
|
||||||
# that it isn't double rendered in the display loop
|
# that it isn't double rendered in the display loop
|
||||||
# above since we do a maxmin calc on the volume data to
|
# above since we do a maxmin calc on the volume data to
|
||||||
# determine if auto-range adjustements should be made.
|
# determine if auto-range adjustements should be made.
|
||||||
# linked.subplots.pop('volume', None)
|
# rt_linked.subplots.pop('volume', None)
|
||||||
|
|
||||||
# TODO: make this not so shit XD
|
# TODO: make this not so shit XD
|
||||||
# close group status
|
# close group status
|
||||||
sbar._status_groups[loading_sym_key][1]()
|
sbar._status_groups[loading_sym_key][1]()
|
||||||
|
|
||||||
|
hist_linked.graphics_cycle()
|
||||||
|
await trio.sleep(0)
|
||||||
|
|
||||||
|
bars_in_mem = int(len(hist_ohlcv.array))
|
||||||
|
hist_chart.default_view(
|
||||||
|
bars_from_y=bars_in_mem, # size to data
|
||||||
|
# push it 1/16th away from the y-axis
|
||||||
|
y_offset=round(bars_in_mem / 16),
|
||||||
|
)
|
||||||
|
godwidget.resize_all()
|
||||||
|
|
||||||
# let the app run.. bby
|
# let the app run.. bby
|
||||||
# linked.graphics_cycle()
|
|
||||||
await trio.sleep_forever()
|
await trio.sleep_forever()
|
||||||
|
|
|
@ -18,8 +18,12 @@
|
||||||
Higher level annotation editors.
|
Higher level annotation editors.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from dataclasses import dataclass, field
|
from __future__ import annotations
|
||||||
from typing import Optional
|
from collections import defaultdict
|
||||||
|
from typing import (
|
||||||
|
Optional,
|
||||||
|
TYPE_CHECKING
|
||||||
|
)
|
||||||
|
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from pyqtgraph import ViewBox, Point, QtCore, QtGui
|
from pyqtgraph import ViewBox, Point, QtCore, QtGui
|
||||||
|
@ -30,28 +34,34 @@ import numpy as np
|
||||||
from ._style import hcolor, _font
|
from ._style import hcolor, _font
|
||||||
from ._lines import LevelLine
|
from ._lines import LevelLine
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
|
from ..data.types import Struct
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ._chart import GodWidget
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
class ArrowEditor(Struct):
|
||||||
class ArrowEditor:
|
|
||||||
|
|
||||||
chart: 'ChartPlotWidget' # noqa
|
godw: GodWidget = None # type: ignore # noqa
|
||||||
_arrows: field(default_factory=dict)
|
_arrows: dict[str, list[pg.ArrowItem]] = {}
|
||||||
|
|
||||||
def add(
|
def add(
|
||||||
self,
|
self,
|
||||||
|
plot: pg.PlotItem,
|
||||||
uid: str,
|
uid: str,
|
||||||
x: float,
|
x: float,
|
||||||
y: float,
|
y: float,
|
||||||
color='default',
|
color='default',
|
||||||
pointing: Optional[str] = None,
|
pointing: Optional[str] = None,
|
||||||
) -> pg.ArrowItem:
|
|
||||||
"""Add an arrow graphic to view at given (x, y).
|
|
||||||
|
|
||||||
"""
|
) -> pg.ArrowItem:
|
||||||
|
'''
|
||||||
|
Add an arrow graphic to view at given (x, y).
|
||||||
|
|
||||||
|
'''
|
||||||
angle = {
|
angle = {
|
||||||
'up': 90,
|
'up': 90,
|
||||||
'down': -90,
|
'down': -90,
|
||||||
|
@ -74,25 +84,25 @@ class ArrowEditor:
|
||||||
brush=pg.mkBrush(hcolor(color)),
|
brush=pg.mkBrush(hcolor(color)),
|
||||||
)
|
)
|
||||||
arrow.setPos(x, y)
|
arrow.setPos(x, y)
|
||||||
|
self._arrows.setdefault(uid, []).append(arrow)
|
||||||
self._arrows[uid] = arrow
|
|
||||||
|
|
||||||
# render to view
|
# render to view
|
||||||
self.chart.plotItem.addItem(arrow)
|
plot.addItem(arrow)
|
||||||
|
|
||||||
return arrow
|
return arrow
|
||||||
|
|
||||||
def remove(self, arrow) -> bool:
|
def remove(self, arrow) -> bool:
|
||||||
self.chart.plotItem.removeItem(arrow)
|
for linked in self.godw.iter_linked():
|
||||||
|
linked.chart.plotItem.removeItem(arrow)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
class LineEditor(Struct):
|
||||||
class LineEditor:
|
'''
|
||||||
'''The great editor of linez.
|
The great editor of linez.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
chart: 'ChartPlotWidget' = None # type: ignore # noqa
|
godw: GodWidget = None # type: ignore # noqa
|
||||||
_order_lines: dict[str, LevelLine] = field(default_factory=dict)
|
_order_lines: defaultdict[str, LevelLine] = defaultdict(list)
|
||||||
_active_staged_line: LevelLine = None
|
_active_staged_line: LevelLine = None
|
||||||
|
|
||||||
def stage_line(
|
def stage_line(
|
||||||
|
@ -100,11 +110,11 @@ class LineEditor:
|
||||||
line: LevelLine,
|
line: LevelLine,
|
||||||
|
|
||||||
) -> LevelLine:
|
) -> LevelLine:
|
||||||
"""Stage a line at the current chart's cursor position
|
'''
|
||||||
|
Stage a line at the current chart's cursor position
|
||||||
and return it.
|
and return it.
|
||||||
|
|
||||||
"""
|
'''
|
||||||
|
|
||||||
# add a "staged" cursor-tracking line to view
|
# add a "staged" cursor-tracking line to view
|
||||||
# and cash it in a a var
|
# and cash it in a a var
|
||||||
if self._active_staged_line:
|
if self._active_staged_line:
|
||||||
|
@ -115,17 +125,25 @@ class LineEditor:
|
||||||
return line
|
return line
|
||||||
|
|
||||||
def unstage_line(self) -> LevelLine:
|
def unstage_line(self) -> LevelLine:
|
||||||
"""Inverse of ``.stage_line()``.
|
'''
|
||||||
|
Inverse of ``.stage_line()``.
|
||||||
|
|
||||||
"""
|
'''
|
||||||
# chart = self.chart._cursor.active_plot
|
cursor = self.godw.get_cursor()
|
||||||
# # chart.setCursor(QtCore.Qt.ArrowCursor)
|
if not cursor:
|
||||||
cursor = self.chart.linked.cursor
|
return None
|
||||||
|
|
||||||
# delete "staged" cursor tracking line from view
|
# delete "staged" cursor tracking line from view
|
||||||
line = self._active_staged_line
|
line = self._active_staged_line
|
||||||
if line:
|
if line:
|
||||||
|
try:
|
||||||
cursor._trackers.remove(line)
|
cursor._trackers.remove(line)
|
||||||
|
except KeyError:
|
||||||
|
# when the current cursor doesn't have said line
|
||||||
|
# registered (probably means that user held order mode
|
||||||
|
# key while panning to another view) then we just
|
||||||
|
# ignore the remove error.
|
||||||
|
pass
|
||||||
line.delete()
|
line.delete()
|
||||||
|
|
||||||
self._active_staged_line = None
|
self._active_staged_line = None
|
||||||
|
@ -133,9 +151,9 @@ class LineEditor:
|
||||||
# show the crosshair y line and label
|
# show the crosshair y line and label
|
||||||
cursor.show_xhair()
|
cursor.show_xhair()
|
||||||
|
|
||||||
def submit_line(
|
def submit_lines(
|
||||||
self,
|
self,
|
||||||
line: LevelLine,
|
lines: list[LevelLine],
|
||||||
uuid: str,
|
uuid: str,
|
||||||
|
|
||||||
) -> LevelLine:
|
) -> LevelLine:
|
||||||
|
@ -145,43 +163,46 @@ class LineEditor:
|
||||||
# raise RuntimeError("No line is currently staged!?")
|
# raise RuntimeError("No line is currently staged!?")
|
||||||
|
|
||||||
# for now, until submission reponse arrives
|
# for now, until submission reponse arrives
|
||||||
|
for line in lines:
|
||||||
line.hide_labels()
|
line.hide_labels()
|
||||||
|
|
||||||
# register for later lookup/deletion
|
# register for later lookup/deletion
|
||||||
self._order_lines[uuid] = line
|
self._order_lines[uuid] += lines
|
||||||
|
|
||||||
return line
|
return lines
|
||||||
|
|
||||||
def commit_line(self, uuid: str) -> LevelLine:
|
def commit_line(self, uuid: str) -> list[LevelLine]:
|
||||||
"""Commit a "staged line" to view.
|
'''
|
||||||
|
Commit a "staged line" to view.
|
||||||
|
|
||||||
Submits the line graphic under the cursor as a (new) permanent
|
Submits the line graphic under the cursor as a (new) permanent
|
||||||
graphic in view.
|
graphic in view.
|
||||||
|
|
||||||
"""
|
'''
|
||||||
try:
|
lines = self._order_lines[uuid]
|
||||||
line = self._order_lines[uuid]
|
if lines:
|
||||||
except KeyError:
|
for line in lines:
|
||||||
log.warning(f'No line for {uuid} could be found?')
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
line.show_labels()
|
line.show_labels()
|
||||||
|
line.hide_markers()
|
||||||
|
log.debug(f'Level active for level: {line.value()}')
|
||||||
# TODO: other flashy things to indicate the order is active
|
# TODO: other flashy things to indicate the order is active
|
||||||
|
|
||||||
log.debug(f'Level active for level: {line.value()}')
|
return lines
|
||||||
|
|
||||||
return line
|
|
||||||
|
|
||||||
def lines_under_cursor(self) -> list[LevelLine]:
|
def lines_under_cursor(self) -> list[LevelLine]:
|
||||||
"""Get the line(s) under the cursor position.
|
'''
|
||||||
|
Get the line(s) under the cursor position.
|
||||||
|
|
||||||
"""
|
'''
|
||||||
# Delete any hoverable under the cursor
|
# Delete any hoverable under the cursor
|
||||||
return self.chart.linked.cursor._hovered
|
return self.godw.get_cursor()._hovered
|
||||||
|
|
||||||
def all_lines(self) -> tuple[LevelLine]:
|
def all_lines(self) -> list[LevelLine]:
|
||||||
return tuple(self._order_lines.values())
|
all_lines = []
|
||||||
|
for lines in list(self._order_lines.values()):
|
||||||
|
all_lines.extend(lines)
|
||||||
|
|
||||||
|
return all_lines
|
||||||
|
|
||||||
def remove_line(
|
def remove_line(
|
||||||
self,
|
self,
|
||||||
|
@ -196,26 +217,27 @@ class LineEditor:
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# try to look up line from our registry
|
# try to look up line from our registry
|
||||||
line = self._order_lines.pop(uuid, line)
|
lines = self._order_lines.pop(uuid, None)
|
||||||
if line:
|
if lines:
|
||||||
|
cursor = self.godw.get_cursor()
|
||||||
|
if cursor:
|
||||||
|
for line in lines:
|
||||||
# if hovered remove from cursor set
|
# if hovered remove from cursor set
|
||||||
cursor = self.chart.linked.cursor
|
|
||||||
hovered = cursor._hovered
|
hovered = cursor._hovered
|
||||||
if line in hovered:
|
if line in hovered:
|
||||||
hovered.remove(line)
|
hovered.remove(line)
|
||||||
|
|
||||||
|
log.debug(f'deleting {line} with oid: {uuid}')
|
||||||
|
line.delete()
|
||||||
|
|
||||||
# make sure the xhair doesn't get left off
|
# make sure the xhair doesn't get left off
|
||||||
# just because we never got a un-hover event
|
# just because we never got a un-hover event
|
||||||
cursor.show_xhair()
|
cursor.show_xhair()
|
||||||
|
|
||||||
log.debug(f'deleting {line} with oid: {uuid}')
|
|
||||||
line.delete()
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
log.warning(f'Could not find line for {line}')
|
log.warning(f'Could not find line for {line}')
|
||||||
|
|
||||||
return line
|
return lines
|
||||||
|
|
||||||
|
|
||||||
class SelectRect(QtGui.QGraphicsRectItem):
|
class SelectRect(QtGui.QGraphicsRectItem):
|
||||||
|
|
|
@ -18,10 +18,11 @@
|
||||||
Qt event proxying and processing using ``trio`` mem chans.
|
Qt event proxying and processing using ``trio`` mem chans.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from contextlib import asynccontextmanager, AsyncExitStack
|
from contextlib import asynccontextmanager as acm
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
|
from tractor.trionics import gather_contexts
|
||||||
from PyQt5 import QtCore
|
from PyQt5 import QtCore
|
||||||
from PyQt5.QtCore import QEvent, pyqtBoundSignal
|
from PyQt5.QtCore import QEvent, pyqtBoundSignal
|
||||||
from PyQt5.QtWidgets import QWidget
|
from PyQt5.QtWidgets import QWidget
|
||||||
|
@ -155,7 +156,7 @@ class EventRelay(QtCore.QObject):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@acm
|
||||||
async def open_event_stream(
|
async def open_event_stream(
|
||||||
|
|
||||||
source_widget: QWidget,
|
source_widget: QWidget,
|
||||||
|
@ -181,7 +182,7 @@ async def open_event_stream(
|
||||||
source_widget.removeEventFilter(kc)
|
source_widget.removeEventFilter(kc)
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@acm
|
||||||
async def open_signal_handler(
|
async def open_signal_handler(
|
||||||
|
|
||||||
signal: pyqtBoundSignal,
|
signal: pyqtBoundSignal,
|
||||||
|
@ -206,7 +207,7 @@ async def open_signal_handler(
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@acm
|
||||||
async def open_handlers(
|
async def open_handlers(
|
||||||
|
|
||||||
source_widgets: list[QWidget],
|
source_widgets: list[QWidget],
|
||||||
|
@ -215,16 +216,14 @@ async def open_handlers(
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
async with (
|
async with (
|
||||||
trio.open_nursery() as n,
|
trio.open_nursery() as n,
|
||||||
AsyncExitStack() as stack,
|
gather_contexts([
|
||||||
):
|
|
||||||
for widget in source_widgets:
|
|
||||||
|
|
||||||
event_recv_stream = await stack.enter_async_context(
|
|
||||||
open_event_stream(widget, event_types, **kwargs)
|
open_event_stream(widget, event_types, **kwargs)
|
||||||
)
|
for widget in source_widgets
|
||||||
|
]) as streams,
|
||||||
|
):
|
||||||
|
for widget, event_recv_stream in zip(source_widgets, streams):
|
||||||
n.start_soon(async_handler, widget, event_recv_stream)
|
n.start_soon(async_handler, widget, event_recv_stream)
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
|
@ -20,13 +20,16 @@ Trio - Qt integration
|
||||||
Run ``trio`` in guest mode on top of the Qt event loop.
|
Run ``trio`` in guest mode on top of the Qt event loop.
|
||||||
All global Qt runtime settings are mostly defined here.
|
All global Qt runtime settings are mostly defined here.
|
||||||
"""
|
"""
|
||||||
from typing import Tuple, Callable, Dict, Any
|
from typing import (
|
||||||
|
Callable,
|
||||||
|
Any,
|
||||||
|
Type,
|
||||||
|
)
|
||||||
import platform
|
import platform
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
# Qt specific
|
# Qt specific
|
||||||
import PyQt5 # noqa
|
import PyQt5 # noqa
|
||||||
import pyqtgraph as pg
|
|
||||||
from pyqtgraph import QtGui
|
from pyqtgraph import QtGui
|
||||||
from PyQt5 import QtCore
|
from PyQt5 import QtCore
|
||||||
# from PyQt5.QtGui import QLabel, QStatusBar
|
# from PyQt5.QtGui import QLabel, QStatusBar
|
||||||
|
@ -37,7 +40,7 @@ from PyQt5.QtCore import (
|
||||||
)
|
)
|
||||||
import qdarkstyle
|
import qdarkstyle
|
||||||
from qdarkstyle import DarkPalette
|
from qdarkstyle import DarkPalette
|
||||||
# import qdarkgraystyle
|
# import qdarkgraystyle # TODO: play with it
|
||||||
import trio
|
import trio
|
||||||
from outcome import Error
|
from outcome import Error
|
||||||
|
|
||||||
|
@ -72,10 +75,11 @@ if platform.system() == "Windows":
|
||||||
|
|
||||||
def run_qtractor(
|
def run_qtractor(
|
||||||
func: Callable,
|
func: Callable,
|
||||||
args: Tuple,
|
args: tuple,
|
||||||
main_widget: QtGui.QWidget,
|
main_widget_type: Type[QtGui.QWidget],
|
||||||
tractor_kwargs: Dict[str, Any] = {},
|
tractor_kwargs: dict[str, Any] = {},
|
||||||
window_type: QtGui.QMainWindow = None,
|
window_type: QtGui.QMainWindow = None,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
# avoids annoying message when entering debugger from qt loop
|
# avoids annoying message when entering debugger from qt loop
|
||||||
pyqtRemoveInputHook()
|
pyqtRemoveInputHook()
|
||||||
|
@ -156,7 +160,7 @@ def run_qtractor(
|
||||||
# hook into app focus change events
|
# hook into app focus change events
|
||||||
app.focusChanged.connect(window.on_focus_change)
|
app.focusChanged.connect(window.on_focus_change)
|
||||||
|
|
||||||
instance = main_widget()
|
instance = main_widget_type()
|
||||||
instance.window = window
|
instance.window = window
|
||||||
|
|
||||||
# override tractor's defaults
|
# override tractor's defaults
|
||||||
|
@ -178,7 +182,7 @@ def run_qtractor(
|
||||||
# restrict_keyboard_interrupt_to_checkpoints=True,
|
# restrict_keyboard_interrupt_to_checkpoints=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
window.main_widget = main_widget
|
window.godwidget: GodWidget = instance
|
||||||
window.setCentralWidget(instance)
|
window.setCentralWidget(instance)
|
||||||
if is_windows:
|
if is_windows:
|
||||||
window.configure_to_desktop()
|
window.configure_to_desktop()
|
||||||
|
|
|
@ -644,7 +644,7 @@ def mk_fill_status_bar(
|
||||||
|
|
||||||
# TODO: calc this height from the ``ChartnPane``
|
# TODO: calc this height from the ``ChartnPane``
|
||||||
chart_h = round(parent_pane.height() * 5/8)
|
chart_h = round(parent_pane.height() * 5/8)
|
||||||
bar_h = chart_h * 0.375
|
bar_h = chart_h * 0.375*0.9
|
||||||
|
|
||||||
# TODO: once things are sized to screen
|
# TODO: once things are sized to screen
|
||||||
bar_label_font_size = label_font_size or _font.px_size - 2
|
bar_label_font_size = label_font_size or _font.px_size - 2
|
||||||
|
|
|
@ -141,13 +141,16 @@ async def handle_viewmode_kb_inputs(
|
||||||
Qt.Key_Space,
|
Qt.Key_Space,
|
||||||
}
|
}
|
||||||
):
|
):
|
||||||
view._chart.linked.godwidget.search.focus()
|
godw = view._chart.linked.godwidget
|
||||||
|
godw.hist_linked.resize_sidepanes(from_linked=godw.rt_linked)
|
||||||
|
godw.search.focus()
|
||||||
|
|
||||||
# esc and ctrl-c
|
# esc and ctrl-c
|
||||||
if key == Qt.Key_Escape or (ctrl and key == Qt.Key_C):
|
if key == Qt.Key_Escape or (ctrl and key == Qt.Key_C):
|
||||||
# ctrl-c as cancel
|
# ctrl-c as cancel
|
||||||
# https://forum.qt.io/topic/532/how-to-catch-ctrl-c-on-a-widget/9
|
# https://forum.qt.io/topic/532/how-to-catch-ctrl-c-on-a-widget/9
|
||||||
view.select_box.clear()
|
view.select_box.clear()
|
||||||
|
view.linked.focus()
|
||||||
|
|
||||||
# cancel order or clear graphics
|
# cancel order or clear graphics
|
||||||
if key == Qt.Key_C or key == Qt.Key_Delete:
|
if key == Qt.Key_C or key == Qt.Key_Delete:
|
||||||
|
@ -178,17 +181,17 @@ async def handle_viewmode_kb_inputs(
|
||||||
if key in pressed:
|
if key in pressed:
|
||||||
pressed.remove(key)
|
pressed.remove(key)
|
||||||
|
|
||||||
# QUERY/QUOTE MODE #
|
# QUERY/QUOTE MODE
|
||||||
|
# ----------------
|
||||||
if {Qt.Key_Q}.intersection(pressed):
|
if {Qt.Key_Q}.intersection(pressed):
|
||||||
|
|
||||||
view.linkedsplits.cursor.in_query_mode = True
|
view.linked.cursor.in_query_mode = True
|
||||||
|
|
||||||
else:
|
else:
|
||||||
view.linkedsplits.cursor.in_query_mode = False
|
view.linked.cursor.in_query_mode = False
|
||||||
|
|
||||||
# SELECTION MODE
|
# SELECTION MODE
|
||||||
# --------------
|
# --------------
|
||||||
|
|
||||||
if shift:
|
if shift:
|
||||||
if view.state['mouseMode'] == ViewBox.PanMode:
|
if view.state['mouseMode'] == ViewBox.PanMode:
|
||||||
view.setMouseMode(ViewBox.RectMode)
|
view.setMouseMode(ViewBox.RectMode)
|
||||||
|
@ -209,14 +212,22 @@ async def handle_viewmode_kb_inputs(
|
||||||
|
|
||||||
# ORDER MODE
|
# ORDER MODE
|
||||||
# ----------
|
# ----------
|
||||||
|
|
||||||
# live vs. dark trigger + an action {buy, sell, alert}
|
# live vs. dark trigger + an action {buy, sell, alert}
|
||||||
order_keys_pressed = ORDER_MODE.intersection(pressed)
|
order_keys_pressed = ORDER_MODE.intersection(pressed)
|
||||||
|
|
||||||
if order_keys_pressed:
|
if order_keys_pressed:
|
||||||
|
|
||||||
# show the pp size label
|
# TODO: it seems like maybe the composition should be
|
||||||
order_mode.current_pp.show()
|
# reversed here? Like, maybe we should have the nav have
|
||||||
|
# access to the pos state and then make encapsulated logic
|
||||||
|
# that shows the right stuff on screen instead or order mode
|
||||||
|
# and position-related abstractions doing this?
|
||||||
|
|
||||||
|
# show the pp size label only if there is
|
||||||
|
# a non-zero pos existing
|
||||||
|
tracker = order_mode.current_pp
|
||||||
|
if tracker.live_pp.size:
|
||||||
|
tracker.nav.show()
|
||||||
|
|
||||||
# TODO: show pp config mini-params in status bar widget
|
# TODO: show pp config mini-params in status bar widget
|
||||||
# mode.pp_config.show()
|
# mode.pp_config.show()
|
||||||
|
@ -257,8 +268,8 @@ async def handle_viewmode_kb_inputs(
|
||||||
Qt.Key_S in pressed or
|
Qt.Key_S in pressed or
|
||||||
order_keys_pressed or
|
order_keys_pressed or
|
||||||
Qt.Key_O in pressed
|
Qt.Key_O in pressed
|
||||||
) and
|
)
|
||||||
key in NUMBER_LINE
|
and key in NUMBER_LINE
|
||||||
):
|
):
|
||||||
# hot key to set order slots size.
|
# hot key to set order slots size.
|
||||||
# change edit field to current number line value,
|
# change edit field to current number line value,
|
||||||
|
@ -276,7 +287,7 @@ async def handle_viewmode_kb_inputs(
|
||||||
else: # none active
|
else: # none active
|
||||||
|
|
||||||
# hide pp label
|
# hide pp label
|
||||||
order_mode.current_pp.hide_info()
|
order_mode.current_pp.nav.hide_info()
|
||||||
|
|
||||||
# if none are pressed, remove "staged" level
|
# if none are pressed, remove "staged" level
|
||||||
# line under cursor position
|
# line under cursor position
|
||||||
|
@ -373,7 +384,7 @@ class ChartView(ViewBox):
|
||||||
y=True,
|
y=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.linkedsplits = None
|
self.linked = None
|
||||||
self._chart: 'ChartPlotWidget' = None # noqa
|
self._chart: 'ChartPlotWidget' = None # noqa
|
||||||
|
|
||||||
# add our selection box annotator
|
# add our selection box annotator
|
||||||
|
@ -484,7 +495,7 @@ class ChartView(ViewBox):
|
||||||
else:
|
else:
|
||||||
mask = self.state['mouseEnabled'][:]
|
mask = self.state['mouseEnabled'][:]
|
||||||
|
|
||||||
chart = self.linkedsplits.chart
|
chart = self.linked.chart
|
||||||
|
|
||||||
# don't zoom more then the min points setting
|
# don't zoom more then the min points setting
|
||||||
l, lbar, rbar, r = chart.bars_range()
|
l, lbar, rbar, r = chart.bars_range()
|
||||||
|
@ -919,7 +930,7 @@ 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
|
||||||
linked = self.linkedsplits
|
linked = self.linked
|
||||||
plots = linked.subplots | {chart.name: chart}
|
plots = linked.subplots | {chart.name: chart}
|
||||||
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,9 +18,14 @@
|
||||||
Lines for orders, alerts, L2.
|
Lines for orders, alerts, L2.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from math import floor
|
from math import floor
|
||||||
from typing import Optional, Callable
|
from typing import (
|
||||||
|
Optional,
|
||||||
|
Callable,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
)
|
||||||
|
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from pyqtgraph import Point, functions as fn
|
from pyqtgraph import Point, functions as fn
|
||||||
|
@ -37,6 +42,9 @@ from ..calc import humanize
|
||||||
from ._label import Label
|
from ._label import Label
|
||||||
from ._style import hcolor, _font
|
from ._style import hcolor, _font
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ._cursor import Cursor
|
||||||
|
|
||||||
|
|
||||||
# TODO: probably worth investigating if we can
|
# TODO: probably worth investigating if we can
|
||||||
# make .boundingRect() faster:
|
# make .boundingRect() faster:
|
||||||
|
@ -84,7 +92,7 @@ class LevelLine(pg.InfiniteLine):
|
||||||
|
|
||||||
self._marker = None
|
self._marker = None
|
||||||
self.only_show_markers_on_hover = only_show_markers_on_hover
|
self.only_show_markers_on_hover = only_show_markers_on_hover
|
||||||
self.show_markers: bool = True # presuming the line is hovered at init
|
self.track_marker_pos: bool = False
|
||||||
|
|
||||||
# should line go all the way to far end or leave a "margin"
|
# should line go all the way to far end or leave a "margin"
|
||||||
# space for other graphics (eg. L1 book)
|
# space for other graphics (eg. L1 book)
|
||||||
|
@ -122,6 +130,9 @@ class LevelLine(pg.InfiniteLine):
|
||||||
self._y_incr_mult = 1 / chart.linked.symbol.tick_size
|
self._y_incr_mult = 1 / chart.linked.symbol.tick_size
|
||||||
self._right_end_sc: float = 0
|
self._right_end_sc: float = 0
|
||||||
|
|
||||||
|
# use px caching
|
||||||
|
self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
|
||||||
|
|
||||||
def txt_offsets(self) -> tuple[int, int]:
|
def txt_offsets(self) -> tuple[int, int]:
|
||||||
return 0, 0
|
return 0, 0
|
||||||
|
|
||||||
|
@ -216,20 +227,23 @@ class LevelLine(pg.InfiniteLine):
|
||||||
y: float
|
y: float
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''Chart coordinates cursor tracking callback.
|
'''
|
||||||
|
Chart coordinates cursor tracking callback.
|
||||||
|
|
||||||
this is called by our ``Cursor`` type once this line is set to
|
this is called by our ``Cursor`` type once this line is set to
|
||||||
track the cursor: for every movement this callback is invoked to
|
track the cursor: for every movement this callback is invoked to
|
||||||
reposition the line with the current view coordinates.
|
reposition the line with the current view coordinates.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
self.movable = True
|
self.movable = True
|
||||||
self.set_level(y) # implictly calls reposition handler
|
self.set_level(y) # implictly calls reposition handler
|
||||||
|
|
||||||
def mouseDragEvent(self, ev):
|
def mouseDragEvent(self, ev):
|
||||||
"""Override the ``InfiniteLine`` handler since we need more
|
'''
|
||||||
|
Override the ``InfiniteLine`` handler since we need more
|
||||||
detailed control and start end signalling.
|
detailed control and start end signalling.
|
||||||
|
|
||||||
"""
|
'''
|
||||||
cursor = self._chart.linked.cursor
|
cursor = self._chart.linked.cursor
|
||||||
|
|
||||||
# hide y-crosshair
|
# hide y-crosshair
|
||||||
|
@ -281,10 +295,20 @@ class LevelLine(pg.InfiniteLine):
|
||||||
# show y-crosshair again
|
# show y-crosshair again
|
||||||
cursor.show_xhair()
|
cursor.show_xhair()
|
||||||
|
|
||||||
def delete(self) -> None:
|
def get_cursor(self) -> Optional[Cursor]:
|
||||||
"""Remove this line from containing chart/view/scene.
|
|
||||||
|
|
||||||
"""
|
chart = self._chart
|
||||||
|
cur = chart.linked.cursor
|
||||||
|
if self in cur._hovered:
|
||||||
|
return cur
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def delete(self) -> None:
|
||||||
|
'''
|
||||||
|
Remove this line from containing chart/view/scene.
|
||||||
|
|
||||||
|
'''
|
||||||
scene = self.scene()
|
scene = self.scene()
|
||||||
if scene:
|
if scene:
|
||||||
for label in self._labels:
|
for label in self._labels:
|
||||||
|
@ -298,9 +322,8 @@ class LevelLine(pg.InfiniteLine):
|
||||||
|
|
||||||
# remove from chart/cursor states
|
# remove from chart/cursor states
|
||||||
chart = self._chart
|
chart = self._chart
|
||||||
cur = chart.linked.cursor
|
cur = self.get_cursor()
|
||||||
|
if cur:
|
||||||
if self in cur._hovered:
|
|
||||||
cur._hovered.remove(self)
|
cur._hovered.remove(self)
|
||||||
|
|
||||||
chart.plotItem.removeItem(self)
|
chart.plotItem.removeItem(self)
|
||||||
|
@ -308,8 +331,8 @@ class LevelLine(pg.InfiniteLine):
|
||||||
def mouseDoubleClickEvent(
|
def mouseDoubleClickEvent(
|
||||||
self,
|
self,
|
||||||
ev: QtGui.QMouseEvent,
|
ev: QtGui.QMouseEvent,
|
||||||
) -> None:
|
|
||||||
|
|
||||||
|
) -> None:
|
||||||
# TODO: enter labels edit mode
|
# TODO: enter labels edit mode
|
||||||
print(f'double click {ev}')
|
print(f'double click {ev}')
|
||||||
|
|
||||||
|
@ -334,30 +357,22 @@ class LevelLine(pg.InfiniteLine):
|
||||||
|
|
||||||
line_end, marker_right, r_axis_x = self._chart.marker_right_points()
|
line_end, marker_right, r_axis_x = self._chart.marker_right_points()
|
||||||
|
|
||||||
if self.show_markers and self.markers:
|
# (legacy) NOTE: at one point this seemed slower when moving around
|
||||||
|
# order lines.. not sure if that's still true or why but we've
|
||||||
p.setPen(self.pen)
|
# dropped the original hacky `.pain()` transform stuff for inf
|
||||||
qgo_draw_markers(
|
# line markers now - check the git history if it needs to be
|
||||||
self.markers,
|
# reverted.
|
||||||
self.pen.color(),
|
if self._marker:
|
||||||
p,
|
if self.track_marker_pos:
|
||||||
vb_left,
|
# make the line end at the marker's x pos
|
||||||
vb_right,
|
line_end = marker_right = self._marker.pos().x()
|
||||||
marker_right,
|
|
||||||
)
|
|
||||||
# marker_size = self.markers[0][2]
|
|
||||||
self._maxMarkerSize = max([m[2] / 2. for m in self.markers])
|
|
||||||
|
|
||||||
# this seems slower when moving around
|
|
||||||
# order lines.. not sure wtf is up with that.
|
|
||||||
# for now we're just using it on the position line.
|
|
||||||
elif self._marker:
|
|
||||||
|
|
||||||
# TODO: make this label update part of a scene-aware-marker
|
# TODO: make this label update part of a scene-aware-marker
|
||||||
# composed annotation
|
# composed annotation
|
||||||
self._marker.setPos(
|
self._marker.setPos(
|
||||||
QPointF(marker_right, self.scene_y())
|
QPointF(marker_right, self.scene_y())
|
||||||
)
|
)
|
||||||
|
|
||||||
if hasattr(self._marker, 'label'):
|
if hasattr(self._marker, 'label'):
|
||||||
self._marker.label.update()
|
self._marker.label.update()
|
||||||
|
|
||||||
|
@ -379,16 +394,14 @@ class LevelLine(pg.InfiniteLine):
|
||||||
|
|
||||||
def hide(self) -> None:
|
def hide(self) -> None:
|
||||||
super().hide()
|
super().hide()
|
||||||
if self._marker:
|
mkr = self._marker
|
||||||
self._marker.hide()
|
if mkr:
|
||||||
# needed for ``order_line()`` lines currently
|
mkr.hide()
|
||||||
self._marker.label.hide()
|
|
||||||
|
|
||||||
def show(self) -> None:
|
def show(self) -> None:
|
||||||
super().show()
|
super().show()
|
||||||
if self._marker:
|
if self._marker:
|
||||||
self._marker.show()
|
self._marker.show()
|
||||||
# self._marker.label.show()
|
|
||||||
|
|
||||||
def scene_y(self) -> float:
|
def scene_y(self) -> float:
|
||||||
return self.getViewBox().mapFromView(
|
return self.getViewBox().mapFromView(
|
||||||
|
@ -433,17 +446,16 @@ class LevelLine(pg.InfiniteLine):
|
||||||
cur = self._chart.linked.cursor
|
cur = self._chart.linked.cursor
|
||||||
|
|
||||||
# hovered
|
# hovered
|
||||||
if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton):
|
if (
|
||||||
|
not ev.isExit()
|
||||||
|
and ev.acceptDrags(QtCore.Qt.LeftButton)
|
||||||
|
):
|
||||||
# if already hovered we don't need to run again
|
# if already hovered we don't need to run again
|
||||||
if self.mouseHovering is True:
|
if self.mouseHovering is True:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.only_show_markers_on_hover:
|
if self.only_show_markers_on_hover:
|
||||||
self.show_markers = True
|
self.show_markers()
|
||||||
|
|
||||||
if self._marker:
|
|
||||||
self._marker.show()
|
|
||||||
|
|
||||||
# highlight if so configured
|
# highlight if so configured
|
||||||
if self.highlight_on_hover:
|
if self.highlight_on_hover:
|
||||||
|
@ -486,11 +498,7 @@ class LevelLine(pg.InfiniteLine):
|
||||||
cur._hovered.remove(self)
|
cur._hovered.remove(self)
|
||||||
|
|
||||||
if self.only_show_markers_on_hover:
|
if self.only_show_markers_on_hover:
|
||||||
self.show_markers = False
|
self.hide_markers()
|
||||||
|
|
||||||
if self._marker:
|
|
||||||
self._marker.hide()
|
|
||||||
self._marker.label.hide()
|
|
||||||
|
|
||||||
if self not in cur._trackers:
|
if self not in cur._trackers:
|
||||||
cur.show_xhair(y_label_level=self.value())
|
cur.show_xhair(y_label_level=self.value())
|
||||||
|
@ -502,6 +510,15 @@ class LevelLine(pg.InfiniteLine):
|
||||||
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
def hide_markers(self) -> None:
|
||||||
|
if self._marker:
|
||||||
|
self._marker.hide()
|
||||||
|
self._marker.label.hide()
|
||||||
|
|
||||||
|
def show_markers(self) -> None:
|
||||||
|
if self._marker:
|
||||||
|
self._marker.show()
|
||||||
|
|
||||||
|
|
||||||
def level_line(
|
def level_line(
|
||||||
|
|
||||||
|
@ -522,9 +539,10 @@ def level_line(
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
|
||||||
) -> LevelLine:
|
) -> LevelLine:
|
||||||
"""Convenience routine to add a styled horizontal line to a plot.
|
'''
|
||||||
|
Convenience routine to add a styled horizontal line to a plot.
|
||||||
|
|
||||||
"""
|
'''
|
||||||
hl_color = color + '_light' if highlight_on_hover else color
|
hl_color = color + '_light' if highlight_on_hover else color
|
||||||
|
|
||||||
line = LevelLine(
|
line = LevelLine(
|
||||||
|
@ -706,7 +724,7 @@ def order_line(
|
||||||
marker = LevelMarker(
|
marker = LevelMarker(
|
||||||
chart=chart,
|
chart=chart,
|
||||||
style=marker_style,
|
style=marker_style,
|
||||||
get_level=line.value,
|
get_level=line.value, # callback
|
||||||
size=marker_size,
|
size=marker_size,
|
||||||
keep_in_view=False,
|
keep_in_view=False,
|
||||||
)
|
)
|
||||||
|
@ -715,7 +733,8 @@ def order_line(
|
||||||
marker = line.add_marker(marker)
|
marker = line.add_marker(marker)
|
||||||
|
|
||||||
# XXX: DON'T COMMENT THIS!
|
# XXX: DON'T COMMENT THIS!
|
||||||
# this fixes it the artifact issue! .. of course, bounding rect stuff
|
# this fixes it the artifact issue!
|
||||||
|
# .. of course, bounding rect stuff
|
||||||
line._maxMarkerSize = marker_size
|
line._maxMarkerSize = marker_size
|
||||||
|
|
||||||
assert line._marker is marker
|
assert line._marker is marker
|
||||||
|
@ -736,7 +755,8 @@ def order_line(
|
||||||
|
|
||||||
if action != 'alert':
|
if action != 'alert':
|
||||||
|
|
||||||
# add a partial position label if we also added a level marker
|
# add a partial position label if we also added a level
|
||||||
|
# marker
|
||||||
pp_size_label = Label(
|
pp_size_label = Label(
|
||||||
view=view,
|
view=view,
|
||||||
color=line.color,
|
color=line.color,
|
||||||
|
@ -770,9 +790,9 @@ def order_line(
|
||||||
# XXX: without this the pp proportion label next the marker
|
# XXX: without this the pp proportion label next the marker
|
||||||
# seems to lag? this is the same issue we had with position
|
# seems to lag? this is the same issue we had with position
|
||||||
# lines which we handle with ``.update_graphcis()``.
|
# lines which we handle with ``.update_graphcis()``.
|
||||||
# marker._on_paint=lambda marker: pp_size_label.update()
|
|
||||||
marker._on_paint = lambda marker: pp_size_label.update()
|
marker._on_paint = lambda marker: pp_size_label.update()
|
||||||
|
|
||||||
|
# XXX: THIS IS AN UNTYPED MONKEY PATCH!?!?!
|
||||||
marker.label = label
|
marker.label = label
|
||||||
|
|
||||||
# sanity check
|
# sanity check
|
||||||
|
|
|
@ -23,7 +23,11 @@ from copy import copy
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from math import floor, copysign
|
from math import floor, copysign
|
||||||
from typing import Optional
|
from typing import (
|
||||||
|
Callable,
|
||||||
|
Optional,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# from PyQt5.QtWidgets import QStyle
|
# from PyQt5.QtWidgets import QStyle
|
||||||
|
@ -41,12 +45,18 @@ from ..calc import humanize, pnl, puterize
|
||||||
from ..clearing._allocate import Allocator, Position
|
from ..clearing._allocate import Allocator, Position
|
||||||
from ..data._normalize import iterticks
|
from ..data._normalize import iterticks
|
||||||
from ..data.feed import Feed
|
from ..data.feed import Feed
|
||||||
|
from ..data.types import Struct
|
||||||
from ._label import Label
|
from ._label import Label
|
||||||
from ._lines import LevelLine, order_line
|
from ._lines import LevelLine, order_line
|
||||||
from ._style import _font
|
from ._style import _font
|
||||||
from ._forms import FieldsForm, FillStatusBar, QLabel
|
from ._forms import FieldsForm, FillStatusBar, QLabel
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ._chart import (
|
||||||
|
ChartPlotWidget,
|
||||||
|
)
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
_pnl_tasks: dict[str, bool] = {}
|
_pnl_tasks: dict[str, bool] = {}
|
||||||
|
|
||||||
|
@ -58,7 +68,8 @@ async def update_pnl_from_feed(
|
||||||
tracker: PositionTracker,
|
tracker: PositionTracker,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''Real-time display the current pp's PnL in the appropriate label.
|
'''
|
||||||
|
Real-time display the current pp's PnL in the appropriate label.
|
||||||
|
|
||||||
``ValueError`` if this task is spawned where there is a net-zero pp.
|
``ValueError`` if this task is spawned where there is a net-zero pp.
|
||||||
|
|
||||||
|
@ -67,7 +78,7 @@ async def update_pnl_from_feed(
|
||||||
|
|
||||||
pp = order_mode.current_pp
|
pp = order_mode.current_pp
|
||||||
live = pp.live_pp
|
live = pp.live_pp
|
||||||
key = live.symbol.key
|
key = live.symbol.front_fqsn()
|
||||||
|
|
||||||
log.info(f'Starting pnl display for {pp.alloc.account}')
|
log.info(f'Starting pnl display for {pp.alloc.account}')
|
||||||
|
|
||||||
|
@ -168,12 +179,12 @@ class SettingsPane:
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
Try to apply some input setting (by the user), revert to previous setting if it fails
|
Try to apply some input setting (by the user), revert to
|
||||||
display new value if applied.
|
previous setting if it fails display new value if applied.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
self.apply_setting(key, value)
|
self.apply_setting(key, value)
|
||||||
self.update_status_ui(pp=self.order_mode.current_pp)
|
self.update_status_ui(self.order_mode.current_pp)
|
||||||
|
|
||||||
def apply_setting(
|
def apply_setting(
|
||||||
self,
|
self,
|
||||||
|
@ -195,7 +206,7 @@ class SettingsPane:
|
||||||
|
|
||||||
# hide details on the old selection
|
# hide details on the old selection
|
||||||
old_tracker = mode.current_pp
|
old_tracker = mode.current_pp
|
||||||
old_tracker.hide_info()
|
old_tracker.nav.hide_info()
|
||||||
|
|
||||||
# re-assign the order mode tracker
|
# re-assign the order mode tracker
|
||||||
account_name = value
|
account_name = value
|
||||||
|
@ -205,7 +216,7 @@ class SettingsPane:
|
||||||
# a ``brokerd`) then error and switch back to the last
|
# a ``brokerd`) then error and switch back to the last
|
||||||
# selection.
|
# selection.
|
||||||
if tracker is None:
|
if tracker is None:
|
||||||
sym = old_tracker.chart.linked.symbol.key
|
sym = old_tracker.charts[0].linked.symbol.key
|
||||||
log.error(
|
log.error(
|
||||||
f'Account `{account_name}` can not be set for {sym}'
|
f'Account `{account_name}` can not be set for {sym}'
|
||||||
)
|
)
|
||||||
|
@ -216,8 +227,8 @@ class SettingsPane:
|
||||||
self.order_mode.current_pp = tracker
|
self.order_mode.current_pp = tracker
|
||||||
assert tracker.alloc.account == account_name
|
assert tracker.alloc.account == account_name
|
||||||
self.form.fields['account'].setCurrentText(account_name)
|
self.form.fields['account'].setCurrentText(account_name)
|
||||||
tracker.show()
|
tracker.nav.show()
|
||||||
tracker.hide_info()
|
tracker.nav.hide_info()
|
||||||
|
|
||||||
self.display_pnl(tracker)
|
self.display_pnl(tracker)
|
||||||
|
|
||||||
|
@ -251,7 +262,9 @@ class SettingsPane:
|
||||||
log.error(
|
log.error(
|
||||||
f'limit must > then current pp: {dsize}'
|
f'limit must > then current pp: {dsize}'
|
||||||
)
|
)
|
||||||
raise ValueError
|
# reset position size value
|
||||||
|
alloc.currency_limit = dsize
|
||||||
|
return False
|
||||||
|
|
||||||
alloc.currency_limit = value
|
alloc.currency_limit = value
|
||||||
|
|
||||||
|
@ -288,22 +301,29 @@ class SettingsPane:
|
||||||
|
|
||||||
def update_status_ui(
|
def update_status_ui(
|
||||||
self,
|
self,
|
||||||
pp: PositionTracker,
|
tracker: PositionTracker,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
alloc = pp.alloc
|
alloc = tracker.alloc
|
||||||
slots = alloc.slots
|
slots = alloc.slots
|
||||||
used = alloc.slots_used(pp.live_pp)
|
used = alloc.slots_used(tracker.live_pp)
|
||||||
|
size = tracker.live_pp.size
|
||||||
|
dsize = tracker.live_pp.dsize
|
||||||
|
|
||||||
# READ out settings and update the status UI / settings widgets
|
# READ out settings and update the status UI / settings widgets
|
||||||
suffix = {'currency': ' $', 'units': ' u'}[alloc.size_unit]
|
suffix = {'currency': ' $', 'units': ' u'}[alloc.size_unit]
|
||||||
limit = alloc.limit()
|
size_unit, limit = alloc.limit_info()
|
||||||
|
|
||||||
step_size, currency_per_slot = alloc.step_sizes()
|
step_size, currency_per_slot = alloc.step_sizes()
|
||||||
|
|
||||||
if alloc.size_unit == 'currency':
|
if alloc.size_unit == 'currency':
|
||||||
step_size = currency_per_slot
|
step_size = currency_per_slot
|
||||||
|
if dsize >= limit:
|
||||||
|
self.apply_setting('limit', limit)
|
||||||
|
|
||||||
|
elif size >= limit:
|
||||||
|
self.apply_setting('limit', limit)
|
||||||
|
|
||||||
self.step_label.format(
|
self.step_label.format(
|
||||||
step_size=str(humanize(step_size)) + suffix
|
step_size=str(humanize(step_size)) + suffix
|
||||||
|
@ -320,7 +340,7 @@ class SettingsPane:
|
||||||
self.form.fields['limit'].setText(str(limit))
|
self.form.fields['limit'].setText(str(limit))
|
||||||
|
|
||||||
# update of level marker size label based on any new settings
|
# update of level marker size label based on any new settings
|
||||||
pp.update_from_pp()
|
tracker.update_from_pp()
|
||||||
|
|
||||||
# calculate proportion of position size limit
|
# calculate proportion of position size limit
|
||||||
# that exists and display in fill bar
|
# that exists and display in fill bar
|
||||||
|
@ -332,7 +352,7 @@ class SettingsPane:
|
||||||
# min(round(prop * slots), slots)
|
# min(round(prop * slots), slots)
|
||||||
min(used, slots)
|
min(used, slots)
|
||||||
)
|
)
|
||||||
self.update_account_icons({alloc.account: pp.live_pp})
|
self.update_account_icons({alloc.account: tracker.live_pp})
|
||||||
|
|
||||||
def update_account_icons(
|
def update_account_icons(
|
||||||
self,
|
self,
|
||||||
|
@ -358,7 +378,9 @@ class SettingsPane:
|
||||||
tracker: PositionTracker,
|
tracker: PositionTracker,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''Display the PnL for the current symbol and personal positioning (pp).
|
'''
|
||||||
|
Display the PnL for the current symbol and personal positioning
|
||||||
|
(pp).
|
||||||
|
|
||||||
If a position is open start a background task which will
|
If a position is open start a background task which will
|
||||||
real-time update the pnl label in the settings pane.
|
real-time update the pnl label in the settings pane.
|
||||||
|
@ -372,7 +394,7 @@ class SettingsPane:
|
||||||
|
|
||||||
if size:
|
if size:
|
||||||
# last historical close price
|
# last historical close price
|
||||||
last = feed.shm.array[-1][['close']][0]
|
last = feed.rt_shm.array[-1][['close']][0]
|
||||||
pnl_value = copysign(1, size) * pnl(
|
pnl_value = copysign(1, size) * pnl(
|
||||||
tracker.live_pp.ppu,
|
tracker.live_pp.ppu,
|
||||||
last,
|
last,
|
||||||
|
@ -380,8 +402,9 @@ class SettingsPane:
|
||||||
|
|
||||||
# maybe start update task
|
# maybe start update task
|
||||||
global _pnl_tasks
|
global _pnl_tasks
|
||||||
if sym.key not in _pnl_tasks:
|
fqsn = sym.front_fqsn()
|
||||||
_pnl_tasks[sym.key] = True
|
if fqsn not in _pnl_tasks:
|
||||||
|
_pnl_tasks[fqsn] = True
|
||||||
self.order_mode.nursery.start_soon(
|
self.order_mode.nursery.start_soon(
|
||||||
update_pnl_from_feed,
|
update_pnl_from_feed,
|
||||||
feed,
|
feed,
|
||||||
|
@ -393,15 +416,15 @@ class SettingsPane:
|
||||||
self.pnl_label.format(pnl=pnl_value)
|
self.pnl_label.format(pnl=pnl_value)
|
||||||
|
|
||||||
|
|
||||||
def position_line(
|
def pp_line(
|
||||||
|
|
||||||
chart: 'ChartPlotWidget', # noqa
|
chart: ChartPlotWidget, # noqa
|
||||||
size: float,
|
size: float,
|
||||||
level: float,
|
level: float,
|
||||||
color: str,
|
color: str,
|
||||||
|
marker: LevelMarker,
|
||||||
|
|
||||||
orient_v: str = 'bottom',
|
orient_v: str = 'bottom',
|
||||||
marker: Optional[LevelMarker] = None,
|
|
||||||
|
|
||||||
) -> LevelLine:
|
) -> LevelLine:
|
||||||
'''
|
'''
|
||||||
|
@ -432,16 +455,7 @@ def position_line(
|
||||||
show_markers=False,
|
show_markers=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
if marker:
|
# TODO: use `LevelLine.add_marker()`` for this instead?
|
||||||
# configure marker to position data
|
|
||||||
|
|
||||||
if size > 0: # long
|
|
||||||
style = '|<' # point "up to" the line
|
|
||||||
elif size < 0: # short
|
|
||||||
style = '>|' # point "down to" the line
|
|
||||||
|
|
||||||
marker.style = style
|
|
||||||
|
|
||||||
# set marker color to same as line
|
# set marker color to same as line
|
||||||
marker.setPen(line.currentPen)
|
marker.setPen(line.currentPen)
|
||||||
marker.setBrush(fn.mkBrush(line.currentPen.color()))
|
marker.setBrush(fn.mkBrush(line.currentPen.color()))
|
||||||
|
@ -449,12 +463,13 @@ def position_line(
|
||||||
marker.update()
|
marker.update()
|
||||||
marker.show()
|
marker.show()
|
||||||
|
|
||||||
|
line._marker = marker
|
||||||
|
line.track_marker_pos = True
|
||||||
|
|
||||||
# show position marker on view "edge" when out of view
|
# show position marker on view "edge" when out of view
|
||||||
vb = line.getViewBox()
|
vb = line.getViewBox()
|
||||||
vb.sigRangeChanged.connect(marker.position_in_view)
|
vb.sigRangeChanged.connect(marker.position_in_view)
|
||||||
|
|
||||||
line.set_level(level)
|
|
||||||
|
|
||||||
return line
|
return line
|
||||||
|
|
||||||
|
|
||||||
|
@ -466,68 +481,313 @@ _derivs = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: move into annoate module?
|
||||||
|
def mk_level_marker(
|
||||||
|
chart: ChartPlotWidget,
|
||||||
|
size: float,
|
||||||
|
level: float,
|
||||||
|
on_paint: Callable,
|
||||||
|
|
||||||
|
) -> LevelMarker:
|
||||||
|
'''
|
||||||
|
Allocate and return nan arrow graphics element.
|
||||||
|
|
||||||
|
'''
|
||||||
|
# scale marker size with dpi-aware font size
|
||||||
|
font_size = _font.font.pixelSize()
|
||||||
|
arrow_size = floor(1.375 * font_size)
|
||||||
|
arrow = LevelMarker(
|
||||||
|
chart=chart,
|
||||||
|
style='|<', # actual style is set by caller based on size
|
||||||
|
get_level=level,
|
||||||
|
size=arrow_size,
|
||||||
|
on_paint=on_paint,
|
||||||
|
)
|
||||||
|
arrow.show()
|
||||||
|
return arrow
|
||||||
|
|
||||||
|
|
||||||
|
class Nav(Struct):
|
||||||
|
'''
|
||||||
|
Composite for holding a set of charts and respective (by order)
|
||||||
|
graphics-elements which display position information acting as sort
|
||||||
|
of "navigation" system for a position.
|
||||||
|
|
||||||
|
'''
|
||||||
|
charts: dict[int, ChartPlotWidget]
|
||||||
|
pp_labels: dict[str, Label] = {}
|
||||||
|
size_labels: dict[str, Label] = {}
|
||||||
|
lines: dict[str, Optional[LevelLine]] = {}
|
||||||
|
level_markers: dict[str, Optional[LevelMarker]] = {}
|
||||||
|
color: str = 'default_lightest'
|
||||||
|
|
||||||
|
def update_ui(
|
||||||
|
self,
|
||||||
|
account: str,
|
||||||
|
price: float,
|
||||||
|
size: float,
|
||||||
|
slots_used: float,
|
||||||
|
size_digits: Optional[int] = None,
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Update personal position level line.
|
||||||
|
|
||||||
|
'''
|
||||||
|
for key, chart in self.charts.items():
|
||||||
|
size_digits = size_digits or chart.linked.symbol.lot_size_digits
|
||||||
|
line = self.lines.get(key)
|
||||||
|
level_marker = self.level_markers[key]
|
||||||
|
pp_label = self.pp_labels[key]
|
||||||
|
|
||||||
|
if size:
|
||||||
|
# create and show a pp line if none yet exists
|
||||||
|
if line is None:
|
||||||
|
arrow = self.level_markers[key]
|
||||||
|
line = pp_line(
|
||||||
|
chart=chart,
|
||||||
|
level=price,
|
||||||
|
size=size,
|
||||||
|
color=self.color,
|
||||||
|
marker=arrow,
|
||||||
|
)
|
||||||
|
self.lines[key] = line
|
||||||
|
|
||||||
|
# modify existing indicator line
|
||||||
|
line.set_level(price)
|
||||||
|
|
||||||
|
# update LHS sizing label
|
||||||
|
line.update_labels({
|
||||||
|
'size': size,
|
||||||
|
'size_digits': size_digits,
|
||||||
|
'fiat_size': round(price * size, ndigits=2),
|
||||||
|
|
||||||
|
# TODO: per account lines on a single (or very
|
||||||
|
# related) symbol
|
||||||
|
'account': account,
|
||||||
|
})
|
||||||
|
line.show()
|
||||||
|
|
||||||
|
# always show arrow-marker when a non-zero
|
||||||
|
# pos size.
|
||||||
|
level_marker.show()
|
||||||
|
|
||||||
|
# configure marker to position data
|
||||||
|
if size > 0: # long
|
||||||
|
# point "up to" the line
|
||||||
|
level_marker.style = '|<'
|
||||||
|
|
||||||
|
elif size < 0: # short
|
||||||
|
# point "down to" the line
|
||||||
|
level_marker.style = '>|'
|
||||||
|
|
||||||
|
# remove line from view for a net-zero pos
|
||||||
|
else:
|
||||||
|
self.hide()
|
||||||
|
|
||||||
|
# label updates
|
||||||
|
size_label = self.size_labels[key]
|
||||||
|
size_label.fields['slots_used'] = slots_used
|
||||||
|
size_label.render()
|
||||||
|
|
||||||
|
# set arrow marker to correct level
|
||||||
|
level_marker.level = price
|
||||||
|
|
||||||
|
# these updates are critical to avoid lag on view/scene changes
|
||||||
|
# TODO: couldn't we integrate this into
|
||||||
|
# a ``.inter_ui_elements_and_update()``?
|
||||||
|
level_marker.update() # trigger paint
|
||||||
|
pp_label.update()
|
||||||
|
size_label.update()
|
||||||
|
|
||||||
|
def level(self) -> float:
|
||||||
|
'''
|
||||||
|
Return the "level" value from the underlying ``LevelLine`` which tracks
|
||||||
|
the "average position" price defined the represented position instance.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if self.lines:
|
||||||
|
for key, line in self.lines.items():
|
||||||
|
if line:
|
||||||
|
return line.value()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def iter_ui_elements(self) -> tuple[
|
||||||
|
Label,
|
||||||
|
Label,
|
||||||
|
LevelLine,
|
||||||
|
LevelMarker,
|
||||||
|
]:
|
||||||
|
for key, chart in self.charts.items():
|
||||||
|
yield (
|
||||||
|
self.pp_labels[key],
|
||||||
|
self.size_labels[key],
|
||||||
|
self.lines.get(key),
|
||||||
|
self.level_markers[key],
|
||||||
|
)
|
||||||
|
|
||||||
|
def show(self) -> None:
|
||||||
|
'''
|
||||||
|
Show all UI elements on all managed charts.
|
||||||
|
|
||||||
|
'''
|
||||||
|
for (
|
||||||
|
pp_label,
|
||||||
|
size_label,
|
||||||
|
line,
|
||||||
|
level_marker,
|
||||||
|
) in self.iter_ui_elements():
|
||||||
|
|
||||||
|
# NOTE: be sure to re-trigger arrow/label placement in case
|
||||||
|
# a new sidepane or other widget (like the search bar) was
|
||||||
|
# dynamically swapped into the chart-row-widget-space in
|
||||||
|
# which case we want to reposition in the view but including
|
||||||
|
# the new x-distance added by that sidepane. See details in
|
||||||
|
# ``LevelMarker.position_in_view()`` but more less ``.
|
||||||
|
# ``ChartPlotWidget.self.marker_right_points()`` gets called
|
||||||
|
# which itself eventually calls `.getAxis.pos().x()` and
|
||||||
|
# it's THIS that needs to be called **AFTER** the sidepane
|
||||||
|
# has been added..
|
||||||
|
level_marker.show()
|
||||||
|
level_marker.position_in_view()
|
||||||
|
|
||||||
|
# labels
|
||||||
|
pp_label.show()
|
||||||
|
size_label.show()
|
||||||
|
|
||||||
|
if line:
|
||||||
|
line.show()
|
||||||
|
line.show_labels()
|
||||||
|
|
||||||
|
def hide(self) -> None:
|
||||||
|
for (
|
||||||
|
pp_label,
|
||||||
|
size_label,
|
||||||
|
line,
|
||||||
|
level_marker,
|
||||||
|
) in self.iter_ui_elements():
|
||||||
|
pp_label.hide()
|
||||||
|
level_marker.hide()
|
||||||
|
size_label.hide()
|
||||||
|
if line:
|
||||||
|
line.hide()
|
||||||
|
|
||||||
|
def update_graphics(
|
||||||
|
self,
|
||||||
|
marker: LevelMarker,
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Update all labels callback.
|
||||||
|
|
||||||
|
Meant to be called from the marker ``.paint()``
|
||||||
|
for immediate, lag free label draws.
|
||||||
|
|
||||||
|
'''
|
||||||
|
for (
|
||||||
|
pp_label,
|
||||||
|
size_label,
|
||||||
|
line,
|
||||||
|
level_marker,
|
||||||
|
) in self.iter_ui_elements():
|
||||||
|
|
||||||
|
pp_label.update()
|
||||||
|
size_label.update()
|
||||||
|
|
||||||
|
# XXX: can't call this because it causes a recursive paint/render
|
||||||
|
# level_marker.update()
|
||||||
|
|
||||||
|
def hide_info(self) -> None:
|
||||||
|
'''
|
||||||
|
Hide details (just size label?) of position nav elements.
|
||||||
|
|
||||||
|
'''
|
||||||
|
for (
|
||||||
|
pp_label,
|
||||||
|
size_label,
|
||||||
|
line,
|
||||||
|
level_marker,
|
||||||
|
) in self.iter_ui_elements():
|
||||||
|
|
||||||
|
size_label.hide()
|
||||||
|
if line:
|
||||||
|
line.hide_labels()
|
||||||
|
|
||||||
|
|
||||||
class PositionTracker:
|
class PositionTracker:
|
||||||
'''
|
'''
|
||||||
Track and display real-time positions for a single symbol
|
Track and display real-time positions for a single asset-symbol
|
||||||
over multiple accounts on a single chart.
|
held in a single account, normally shown on a single chart.
|
||||||
|
|
||||||
Graphically composed of a level line and marker as well as labels
|
Graphically composed of a level line and marker as well as labels
|
||||||
for indcating current position information. Updates are made to the
|
for indcating current position information. Updates are made to the
|
||||||
corresponding "settings pane" for the chart's "order mode" UX.
|
corresponding "settings pane" for the chart's "order mode" UX.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# inputs
|
|
||||||
chart: 'ChartPlotWidget' # noqa
|
|
||||||
|
|
||||||
alloc: Allocator
|
alloc: Allocator
|
||||||
startup_pp: Position
|
startup_pp: Position
|
||||||
live_pp: Position
|
live_pp: Position
|
||||||
|
nav: Nav # holds all UI elements across all charts
|
||||||
# allocated
|
|
||||||
pp_label: Label
|
|
||||||
size_label: Label
|
|
||||||
line: Optional[LevelLine] = None
|
|
||||||
|
|
||||||
_color: str = 'default_lightest'
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
chart: 'ChartPlotWidget', # noqa
|
charts: list[ChartPlotWidget],
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
startup_pp: Position,
|
startup_pp: Position,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
self.chart = chart
|
nav = self.nav = Nav(charts={id(chart): chart for chart in charts})
|
||||||
|
|
||||||
self.alloc = alloc
|
self.alloc = alloc
|
||||||
self.startup_pp = startup_pp
|
self.startup_pp = startup_pp
|
||||||
self.live_pp = copy(startup_pp)
|
self.live_pp = copy(startup_pp)
|
||||||
|
|
||||||
|
# TODO: maybe add this as a method ``Nav.add_chart()``
|
||||||
|
# init all UI elements
|
||||||
|
for key, chart in nav.charts.items():
|
||||||
view = chart.getViewBox()
|
view = chart.getViewBox()
|
||||||
|
|
||||||
# literally the 'pp' (pee pee) label that's always in view
|
arrow = mk_level_marker(
|
||||||
self.pp_label = pp_label = Label(
|
chart=chart,
|
||||||
|
size=1,
|
||||||
|
level=nav.level,
|
||||||
|
on_paint=nav.update_graphics,
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: we really need some kinda "spacing" manager for all
|
||||||
|
# this stuff...
|
||||||
|
def offset_from_yaxis() -> float:
|
||||||
|
'''
|
||||||
|
If no L1 labels are present beside the x-axis place
|
||||||
|
the line label offset from the y-axis just enough to avoid
|
||||||
|
label overlap with any sticky labels.
|
||||||
|
|
||||||
|
'''
|
||||||
|
x = chart.marker_right_points()[1]
|
||||||
|
if chart._max_l1_line_len == 0:
|
||||||
|
mkw = pp_label.txt.boundingRect().width()
|
||||||
|
x -= 1.5 * mkw
|
||||||
|
|
||||||
|
return x
|
||||||
|
|
||||||
|
arrow.scene_x = offset_from_yaxis
|
||||||
|
view.scene().addItem(arrow)
|
||||||
|
arrow.hide() # never show on startup
|
||||||
|
nav.level_markers[key] = arrow
|
||||||
|
|
||||||
|
# literally the 'pp' (pee pee) "position price" label that's
|
||||||
|
# always in view
|
||||||
|
pp_label = Label(
|
||||||
view=view,
|
view=view,
|
||||||
fmt_str='pp',
|
fmt_str='pp',
|
||||||
color=self._color,
|
color=nav.color,
|
||||||
update_on_range_change=False,
|
update_on_range_change=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# create placeholder 'up' level arrow
|
|
||||||
self._level_marker = None
|
|
||||||
self._level_marker = self.level_marker(size=1)
|
|
||||||
|
|
||||||
pp_label.scene_anchor = partial(
|
|
||||||
gpath_pin,
|
|
||||||
gpath=self._level_marker,
|
|
||||||
label=pp_label,
|
|
||||||
)
|
|
||||||
pp_label.render()
|
pp_label.render()
|
||||||
|
nav.pp_labels[key] = pp_label
|
||||||
|
|
||||||
self.size_label = size_label = Label(
|
size_label = Label(
|
||||||
view=view,
|
view=view,
|
||||||
color=self._color,
|
color=self.nav.color,
|
||||||
|
|
||||||
# this is "static" label
|
# this is "static" label
|
||||||
# update_on_range_change=False,
|
# update_on_range_change=False,
|
||||||
|
@ -540,11 +800,19 @@ class PositionTracker:
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
size_label.render()
|
size_label.render()
|
||||||
|
|
||||||
size_label.scene_anchor = partial(
|
size_label.scene_anchor = partial(
|
||||||
pp_tight_and_right,
|
pp_tight_and_right,
|
||||||
label=self.pp_label,
|
label=pp_label,
|
||||||
)
|
)
|
||||||
|
nav.size_labels[key] = size_label
|
||||||
|
|
||||||
|
pp_label.scene_anchor = partial(
|
||||||
|
gpath_pin,
|
||||||
|
gpath=arrow,
|
||||||
|
label=pp_label,
|
||||||
|
)
|
||||||
|
|
||||||
|
nav.show()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pane(self) -> FieldsForm:
|
def pane(self) -> FieldsForm:
|
||||||
|
@ -554,21 +822,6 @@ class PositionTracker:
|
||||||
'''
|
'''
|
||||||
return self.chart.linked.godwidget.pp_pane
|
return self.chart.linked.godwidget.pp_pane
|
||||||
|
|
||||||
def update_graphics(
|
|
||||||
self,
|
|
||||||
marker: LevelMarker
|
|
||||||
|
|
||||||
) -> None:
|
|
||||||
'''
|
|
||||||
Update all labels.
|
|
||||||
|
|
||||||
Meant to be called from the maker ``.paint()``
|
|
||||||
for immediate, lag free label draws.
|
|
||||||
|
|
||||||
'''
|
|
||||||
self.pp_label.update()
|
|
||||||
self.size_label.update()
|
|
||||||
|
|
||||||
def update_from_pp(
|
def update_from_pp(
|
||||||
self,
|
self,
|
||||||
position: Optional[Position] = None,
|
position: Optional[Position] = None,
|
||||||
|
@ -621,142 +874,22 @@ class PositionTracker:
|
||||||
if asset_type in _derivs:
|
if asset_type in _derivs:
|
||||||
alloc.slots = alloc.units_limit
|
alloc.slots = alloc.units_limit
|
||||||
|
|
||||||
self.update_line(
|
self.nav.update_ui(
|
||||||
|
self.alloc.account,
|
||||||
pp.ppu,
|
pp.ppu,
|
||||||
pp.size,
|
pp.size,
|
||||||
self.chart.linked.symbol.lot_size_digits,
|
round(alloc.slots_used(pp), ndigits=1), # slots used
|
||||||
)
|
)
|
||||||
|
|
||||||
# label updates
|
if self.live_pp.size:
|
||||||
self.size_label.fields['slots_used'] = round(
|
# print("SHOWING NAV")
|
||||||
alloc.slots_used(pp), ndigits=1)
|
self.nav.show()
|
||||||
self.size_label.render()
|
|
||||||
|
|
||||||
if pp.size == 0:
|
|
||||||
self.hide()
|
|
||||||
|
|
||||||
|
# if pp.size == 0:
|
||||||
else:
|
else:
|
||||||
self._level_marker.level = pp.ppu
|
# print("HIDING NAV")
|
||||||
|
self.nav.hide()
|
||||||
# these updates are critical to avoid lag on view/scene changes
|
|
||||||
self._level_marker.update() # trigger paint
|
|
||||||
self.pp_label.update()
|
|
||||||
self.size_label.update()
|
|
||||||
|
|
||||||
self.show()
|
|
||||||
|
|
||||||
# don't show side and status widgets unless
|
# don't show side and status widgets unless
|
||||||
# order mode is "engaged" (which done via input controls)
|
# order mode is "engaged" (which done via input controls)
|
||||||
self.hide_info()
|
self.nav.hide_info()
|
||||||
|
|
||||||
def level(self) -> float:
|
|
||||||
if self.line:
|
|
||||||
return self.line.value()
|
|
||||||
else:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def show(self) -> None:
|
|
||||||
if self.live_pp.size:
|
|
||||||
self.line.show()
|
|
||||||
self.line.show_labels()
|
|
||||||
|
|
||||||
self._level_marker.show()
|
|
||||||
self.pp_label.show()
|
|
||||||
self.size_label.show()
|
|
||||||
|
|
||||||
def hide(self) -> None:
|
|
||||||
self.pp_label.hide()
|
|
||||||
self._level_marker.hide()
|
|
||||||
self.size_label.hide()
|
|
||||||
if self.line:
|
|
||||||
self.line.hide()
|
|
||||||
|
|
||||||
def hide_info(self) -> None:
|
|
||||||
'''Hide details (right now just size label?) of position.
|
|
||||||
|
|
||||||
'''
|
|
||||||
self.size_label.hide()
|
|
||||||
if self.line:
|
|
||||||
self.line.hide_labels()
|
|
||||||
|
|
||||||
# TODO: move into annoate module
|
|
||||||
def level_marker(
|
|
||||||
self,
|
|
||||||
size: float,
|
|
||||||
|
|
||||||
) -> LevelMarker:
|
|
||||||
|
|
||||||
if self._level_marker:
|
|
||||||
self._level_marker.delete()
|
|
||||||
|
|
||||||
# arrow marker
|
|
||||||
# scale marker size with dpi-aware font size
|
|
||||||
font_size = _font.font.pixelSize()
|
|
||||||
|
|
||||||
# scale marker size with dpi-aware font size
|
|
||||||
arrow_size = floor(1.375 * font_size)
|
|
||||||
|
|
||||||
if size > 0:
|
|
||||||
style = '|<'
|
|
||||||
|
|
||||||
elif size < 0:
|
|
||||||
style = '>|'
|
|
||||||
|
|
||||||
arrow = LevelMarker(
|
|
||||||
chart=self.chart,
|
|
||||||
style=style,
|
|
||||||
get_level=self.level,
|
|
||||||
size=arrow_size,
|
|
||||||
on_paint=self.update_graphics,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.chart.getViewBox().scene().addItem(arrow)
|
|
||||||
arrow.show()
|
|
||||||
|
|
||||||
return arrow
|
|
||||||
|
|
||||||
def update_line(
|
|
||||||
self,
|
|
||||||
price: float,
|
|
||||||
size: float,
|
|
||||||
size_digits: int,
|
|
||||||
|
|
||||||
) -> None:
|
|
||||||
'''Update personal position level line.
|
|
||||||
|
|
||||||
'''
|
|
||||||
# do line update
|
|
||||||
line = self.line
|
|
||||||
|
|
||||||
if size:
|
|
||||||
if line is None:
|
|
||||||
|
|
||||||
# create and show a pp line
|
|
||||||
line = self.line = position_line(
|
|
||||||
chart=self.chart,
|
|
||||||
level=price,
|
|
||||||
size=size,
|
|
||||||
color=self._color,
|
|
||||||
marker=self._level_marker,
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
line.set_level(price)
|
|
||||||
self._level_marker.level = price
|
|
||||||
self._level_marker.update()
|
|
||||||
|
|
||||||
# update LHS sizing label
|
|
||||||
line.update_labels({
|
|
||||||
'size': size,
|
|
||||||
'size_digits': size_digits,
|
|
||||||
'fiat_size': round(price * size, ndigits=2),
|
|
||||||
|
|
||||||
# TODO: per account lines on a single (or very related) symbol
|
|
||||||
'account': self.alloc.account,
|
|
||||||
})
|
|
||||||
line.show()
|
|
||||||
|
|
||||||
elif line: # remove pp line from view if it exists on a net-zero pp
|
|
||||||
line.delete()
|
|
||||||
self.line = None
|
|
||||||
|
|
|
@ -35,9 +35,13 @@ from collections import defaultdict
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import (
|
from typing import (
|
||||||
Optional, Callable,
|
Optional,
|
||||||
Awaitable, Sequence,
|
Callable,
|
||||||
Any, AsyncIterator
|
Awaitable,
|
||||||
|
Sequence,
|
||||||
|
Any,
|
||||||
|
AsyncIterator,
|
||||||
|
Iterator,
|
||||||
)
|
)
|
||||||
import time
|
import time
|
||||||
# from pprint import pformat
|
# from pprint import pformat
|
||||||
|
@ -119,7 +123,7 @@ class CompleterView(QTreeView):
|
||||||
# TODO: size this based on DPI font
|
# TODO: size this based on DPI font
|
||||||
self.setIndentation(_font.px_size)
|
self.setIndentation(_font.px_size)
|
||||||
|
|
||||||
# self.setUniformRowHeights(True)
|
self.setUniformRowHeights(True)
|
||||||
# self.setColumnWidth(0, 3)
|
# self.setColumnWidth(0, 3)
|
||||||
# self.setVerticalBarPolicy(Qt.ScrollBarAlwaysOff)
|
# self.setVerticalBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||||
# self.setSizeAdjustPolicy(QAbstractScrollArea.AdjustIgnored)
|
# self.setSizeAdjustPolicy(QAbstractScrollArea.AdjustIgnored)
|
||||||
|
@ -138,13 +142,15 @@ class CompleterView(QTreeView):
|
||||||
model.setHorizontalHeaderLabels(labels)
|
model.setHorizontalHeaderLabels(labels)
|
||||||
|
|
||||||
self._font_size: int = 0 # pixels
|
self._font_size: int = 0 # pixels
|
||||||
|
self._init: bool = False
|
||||||
|
|
||||||
async def on_pressed(self, idx: QModelIndex) -> None:
|
async def on_pressed(self, idx: QModelIndex) -> None:
|
||||||
'''Mouse pressed on view handler.
|
'''
|
||||||
|
Mouse pressed on view handler.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
search = self.parent()
|
search = self.parent()
|
||||||
await search.chart_current_item(clear_to_cache=False)
|
await search.chart_current_item()
|
||||||
search.focus()
|
search.focus()
|
||||||
|
|
||||||
def set_font_size(self, size: int = 18):
|
def set_font_size(self, size: int = 18):
|
||||||
|
@ -156,56 +162,64 @@ class CompleterView(QTreeView):
|
||||||
|
|
||||||
self.setStyleSheet(f"font: {size}px")
|
self.setStyleSheet(f"font: {size}px")
|
||||||
|
|
||||||
# def resizeEvent(self, event: 'QEvent') -> None:
|
def resize_to_results(
|
||||||
# event.accept()
|
self,
|
||||||
# super().resizeEvent(event)
|
w: Optional[float] = 0,
|
||||||
|
h: Optional[float] = None,
|
||||||
|
|
||||||
def on_resize(self) -> None:
|
) -> None:
|
||||||
'''
|
|
||||||
Resize relay event from god.
|
|
||||||
|
|
||||||
'''
|
|
||||||
self.resize_to_results()
|
|
||||||
|
|
||||||
def resize_to_results(self):
|
|
||||||
model = self.model()
|
model = self.model()
|
||||||
cols = model.columnCount()
|
cols = model.columnCount()
|
||||||
# rows = model.rowCount()
|
cidx = self.selectionModel().currentIndex()
|
||||||
|
rows = model.rowCount()
|
||||||
|
self.expandAll()
|
||||||
|
|
||||||
|
# compute the approx height in pixels needed to include
|
||||||
|
# all result rows in view.
|
||||||
|
row_h = rows_h = self.rowHeight(cidx) * (rows + 1)
|
||||||
|
for idx, item in self.iter_df_rows():
|
||||||
|
row_h = self.rowHeight(idx)
|
||||||
|
rows_h += row_h
|
||||||
|
# print(f'row_h: {row_h}\nrows_h: {rows_h}')
|
||||||
|
|
||||||
|
# TODO: could we just break early here on detection
|
||||||
|
# of ``rows_h >= h``?
|
||||||
|
|
||||||
col_w_tot = 0
|
col_w_tot = 0
|
||||||
for i in range(cols):
|
for i in range(cols):
|
||||||
|
# only slap in a rows's height's worth
|
||||||
|
# of padding once at startup.. no idea
|
||||||
|
if (
|
||||||
|
not self._init
|
||||||
|
and row_h
|
||||||
|
):
|
||||||
|
col_w_tot = row_h
|
||||||
|
self._init = True
|
||||||
|
|
||||||
self.resizeColumnToContents(i)
|
self.resizeColumnToContents(i)
|
||||||
col_w_tot += self.columnWidth(i)
|
col_w_tot += self.columnWidth(i)
|
||||||
|
|
||||||
win = self.window()
|
# NOTE: if the heigh `h` set here is **too large** then the
|
||||||
win_h = win.height()
|
# resize event will perpetually trigger as the window causes
|
||||||
edit_h = self.parent().bar.height()
|
# some kind of recompute of callbacks.. so we have to ensure
|
||||||
sb_h = win.statusBar().height()
|
# it's limited.
|
||||||
|
if h:
|
||||||
|
h: int = round(h)
|
||||||
|
abs_mx = round(0.91 * h)
|
||||||
|
self.setMaximumHeight(abs_mx)
|
||||||
|
|
||||||
# TODO: probably make this more general / less hacky
|
if rows_h <= abs_mx:
|
||||||
# we should figure out the exact number of rows to allow
|
# self.setMinimumHeight(rows_h)
|
||||||
# inclusive of search bar and header "rows", in pixel terms.
|
self.setMinimumHeight(rows_h)
|
||||||
# Eventually when we have an "info" widget below the results we
|
# self.setFixedHeight(rows_h)
|
||||||
# will want space for it and likely terminating the results-view
|
|
||||||
# space **exactly on a row** would be ideal.
|
|
||||||
# if row_px > 0:
|
|
||||||
# rows = ceil(window_h / row_px) - 4
|
|
||||||
# else:
|
|
||||||
# rows = 16
|
|
||||||
# self.setFixedHeight(rows * row_px)
|
|
||||||
# self.resize(self.width(), rows * row_px)
|
|
||||||
|
|
||||||
# NOTE: if the heigh set here is **too large** then the resize
|
else:
|
||||||
# event will perpetually trigger as the window causes some kind
|
self.setMinimumHeight(abs_mx)
|
||||||
# of recompute of callbacks.. so we have to ensure it's limited.
|
|
||||||
h = win_h - (edit_h + 1.666*sb_h)
|
|
||||||
assert h > 0
|
|
||||||
self.setFixedHeight(round(h))
|
|
||||||
|
|
||||||
# size to width of longest result seen thus far
|
# dyncamically size to width of longest result seen
|
||||||
# TODO: should we always dynamically scale to longest result?
|
curr_w = self.width()
|
||||||
if self.width() < col_w_tot:
|
if curr_w < col_w_tot:
|
||||||
self.setFixedWidth(col_w_tot)
|
self.setMinimumWidth(col_w_tot)
|
||||||
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
@ -331,6 +345,23 @@ class CompleterView(QTreeView):
|
||||||
item = model.itemFromIndex(idx)
|
item = model.itemFromIndex(idx)
|
||||||
yield idx, item
|
yield idx, item
|
||||||
|
|
||||||
|
def iter_df_rows(
|
||||||
|
self,
|
||||||
|
iparent: QModelIndex = QModelIndex(),
|
||||||
|
|
||||||
|
) -> Iterator[tuple[QModelIndex, QStandardItem]]:
|
||||||
|
|
||||||
|
model = self.model()
|
||||||
|
isections = model.rowCount(iparent)
|
||||||
|
for i in range(isections):
|
||||||
|
idx = model.index(i, 0, iparent)
|
||||||
|
item = model.itemFromIndex(idx)
|
||||||
|
yield idx, item
|
||||||
|
|
||||||
|
if model.hasChildren(idx):
|
||||||
|
# recursively yield child items depth-first
|
||||||
|
yield from self.iter_df_rows(idx)
|
||||||
|
|
||||||
def find_section(
|
def find_section(
|
||||||
self,
|
self,
|
||||||
section: str,
|
section: str,
|
||||||
|
@ -354,7 +385,8 @@ class CompleterView(QTreeView):
|
||||||
status_field: str = None,
|
status_field: str = None,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''Clear all result-rows from under the depth = 1 section.
|
'''
|
||||||
|
Clear all result-rows from under the depth = 1 section.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
idx = self.find_section(section)
|
idx = self.find_section(section)
|
||||||
|
@ -375,8 +407,6 @@ class CompleterView(QTreeView):
|
||||||
else:
|
else:
|
||||||
model.setItem(idx.row(), 1, QStandardItem())
|
model.setItem(idx.row(), 1, QStandardItem())
|
||||||
|
|
||||||
self.resize_to_results()
|
|
||||||
|
|
||||||
return idx
|
return idx
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
@ -444,9 +474,22 @@ class CompleterView(QTreeView):
|
||||||
|
|
||||||
self.show_matches()
|
self.show_matches()
|
||||||
|
|
||||||
def show_matches(self) -> None:
|
def show_matches(
|
||||||
|
self,
|
||||||
|
wh: Optional[tuple[float, float]] = None,
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
if wh:
|
||||||
|
self.resize_to_results(*wh)
|
||||||
|
else:
|
||||||
|
# case where it's just an update from results and *NOT*
|
||||||
|
# a resize of some higher level parent-container widget.
|
||||||
|
search = self.parent()
|
||||||
|
w, h = search.space_dims()
|
||||||
|
self.resize_to_results(w=w, h=h)
|
||||||
|
|
||||||
self.show()
|
self.show()
|
||||||
self.resize_to_results()
|
|
||||||
|
|
||||||
|
|
||||||
class SearchBar(Edit):
|
class SearchBar(Edit):
|
||||||
|
@ -466,18 +509,15 @@ class SearchBar(Edit):
|
||||||
self.godwidget = godwidget
|
self.godwidget = godwidget
|
||||||
super().__init__(parent, **kwargs)
|
super().__init__(parent, **kwargs)
|
||||||
self.view: CompleterView = view
|
self.view: CompleterView = view
|
||||||
godwidget._widgets[view.mode_name] = view
|
|
||||||
|
|
||||||
def show(self) -> None:
|
|
||||||
super().show()
|
|
||||||
self.view.show_matches()
|
|
||||||
|
|
||||||
def unfocus(self) -> None:
|
def unfocus(self) -> None:
|
||||||
self.parent().hide()
|
self.parent().hide()
|
||||||
self.clearFocus()
|
self.clearFocus()
|
||||||
|
|
||||||
|
def hide(self) -> None:
|
||||||
if self.view:
|
if self.view:
|
||||||
self.view.hide()
|
self.view.hide()
|
||||||
|
super().hide()
|
||||||
|
|
||||||
|
|
||||||
class SearchWidget(QtWidgets.QWidget):
|
class SearchWidget(QtWidgets.QWidget):
|
||||||
|
@ -496,15 +536,16 @@ class SearchWidget(QtWidgets.QWidget):
|
||||||
parent=None,
|
parent=None,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(parent or godwidget)
|
super().__init__(parent)
|
||||||
|
|
||||||
# size it as we specify
|
# size it as we specify
|
||||||
self.setSizePolicy(
|
self.setSizePolicy(
|
||||||
QtWidgets.QSizePolicy.Fixed,
|
QtWidgets.QSizePolicy.Fixed,
|
||||||
QtWidgets.QSizePolicy.Expanding,
|
QtWidgets.QSizePolicy.Fixed,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.godwidget = godwidget
|
self.godwidget = godwidget
|
||||||
|
godwidget.reg_for_resize(self)
|
||||||
|
|
||||||
self.vbox = QtWidgets.QVBoxLayout(self)
|
self.vbox = QtWidgets.QVBoxLayout(self)
|
||||||
self.vbox.setContentsMargins(0, 4, 4, 0)
|
self.vbox.setContentsMargins(0, 4, 4, 0)
|
||||||
|
@ -554,18 +595,23 @@ class SearchWidget(QtWidgets.QWidget):
|
||||||
self.vbox.setAlignment(self.view, Qt.AlignTop | Qt.AlignLeft)
|
self.vbox.setAlignment(self.view, Qt.AlignTop | Qt.AlignLeft)
|
||||||
|
|
||||||
def focus(self) -> None:
|
def focus(self) -> None:
|
||||||
|
self.show()
|
||||||
|
self.bar.focus()
|
||||||
|
|
||||||
if self.view.model().rowCount(QModelIndex()) == 0:
|
def show_only_cache_entries(self) -> None:
|
||||||
# fill cache list if nothing existing
|
'''
|
||||||
|
Clear the search results view and show only cached (aka recently
|
||||||
|
loaded with active data) feeds in the results section.
|
||||||
|
|
||||||
|
'''
|
||||||
|
godw = self.godwidget
|
||||||
self.view.set_section_entries(
|
self.view.set_section_entries(
|
||||||
'cache',
|
'cache',
|
||||||
list(reversed(self.godwidget._chart_cache)),
|
list(reversed(godw._chart_cache)),
|
||||||
|
# remove all other completion results except for cache
|
||||||
clear_all=True,
|
clear_all=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.bar.focus()
|
|
||||||
self.show()
|
|
||||||
|
|
||||||
def get_current_item(self) -> Optional[tuple[str, str]]:
|
def get_current_item(self) -> Optional[tuple[str, str]]:
|
||||||
'''Return the current completer tree selection as
|
'''Return the current completer tree selection as
|
||||||
a tuple ``(parent: str, child: str)`` if valid, else ``None``.
|
a tuple ``(parent: str, child: str)`` if valid, else ``None``.
|
||||||
|
@ -603,7 +649,8 @@ class SearchWidget(QtWidgets.QWidget):
|
||||||
clear_to_cache: bool = True,
|
clear_to_cache: bool = True,
|
||||||
|
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
'''Attempt to load and switch the current selected
|
'''
|
||||||
|
Attempt to load and switch the current selected
|
||||||
completion result to the affiliated chart app.
|
completion result to the affiliated chart app.
|
||||||
|
|
||||||
Return any loaded symbol.
|
Return any loaded symbol.
|
||||||
|
@ -614,11 +661,11 @@ class SearchWidget(QtWidgets.QWidget):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
provider, symbol = value
|
provider, symbol = value
|
||||||
chart = self.godwidget
|
godw = self.godwidget
|
||||||
|
|
||||||
log.info(f'Requesting symbol: {symbol}.{provider}')
|
log.info(f'Requesting symbol: {symbol}.{provider}')
|
||||||
|
|
||||||
await chart.load_symbol(
|
await godw.load_symbol(
|
||||||
provider,
|
provider,
|
||||||
symbol,
|
symbol,
|
||||||
'info',
|
'info',
|
||||||
|
@ -635,18 +682,46 @@ class SearchWidget(QtWidgets.QWidget):
|
||||||
# Re-order the symbol cache on the chart to display in
|
# Re-order the symbol cache on the chart to display in
|
||||||
# LIFO order. this is normally only done internally by
|
# LIFO order. this is normally only done internally by
|
||||||
# the chart on new symbols being loaded into memory
|
# the chart on new symbols being loaded into memory
|
||||||
chart.set_chart_symbol(fqsn, chart.linkedsplits)
|
godw.set_chart_symbol(
|
||||||
|
fqsn, (
|
||||||
self.view.set_section_entries(
|
godw.hist_linked,
|
||||||
'cache',
|
godw.rt_linked,
|
||||||
values=list(reversed(chart._chart_cache)),
|
|
||||||
|
|
||||||
# remove all other completion results except for cache
|
|
||||||
clear_all=True,
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
self.show_only_cache_entries()
|
||||||
|
|
||||||
|
self.bar.focus()
|
||||||
return fqsn
|
return fqsn
|
||||||
|
|
||||||
|
def space_dims(self) -> tuple[float, float]:
|
||||||
|
'''
|
||||||
|
Compute and return the "available space dimentions" for this
|
||||||
|
search widget in terms of px space for results by return the
|
||||||
|
pair of width and height.
|
||||||
|
|
||||||
|
'''
|
||||||
|
# XXX: dun need dis rite?
|
||||||
|
# win = self.window()
|
||||||
|
# win_h = win.height()
|
||||||
|
# sb_h = win.statusBar().height()
|
||||||
|
godw = self.godwidget
|
||||||
|
hl = godw.hist_linked
|
||||||
|
edit_h = self.bar.height()
|
||||||
|
h = hl.height() - edit_h
|
||||||
|
w = hl.width()
|
||||||
|
return w, h
|
||||||
|
|
||||||
|
def on_resize(self) -> None:
|
||||||
|
'''
|
||||||
|
Resize relay event from god, resize all child widgets.
|
||||||
|
|
||||||
|
Right now this is just view to contents and/or the fast chart
|
||||||
|
height.
|
||||||
|
|
||||||
|
'''
|
||||||
|
w, h = self.space_dims()
|
||||||
|
self.bar.view.show_matches(wh=(w, h))
|
||||||
|
|
||||||
|
|
||||||
_search_active: trio.Event = trio.Event()
|
_search_active: trio.Event = trio.Event()
|
||||||
_search_enabled: bool = False
|
_search_enabled: bool = False
|
||||||
|
@ -712,10 +787,11 @@ async def fill_results(
|
||||||
max_pause_time: float = 6/16 + 0.001,
|
max_pause_time: float = 6/16 + 0.001,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Task to search through providers and fill in possible
|
'''
|
||||||
|
Task to search through providers and fill in possible
|
||||||
completion results.
|
completion results.
|
||||||
|
|
||||||
"""
|
'''
|
||||||
global _search_active, _search_enabled, _searcher_cache
|
global _search_active, _search_enabled, _searcher_cache
|
||||||
|
|
||||||
bar = search.bar
|
bar = search.bar
|
||||||
|
@ -729,6 +805,10 @@ async def fill_results(
|
||||||
matches = defaultdict(list)
|
matches = defaultdict(list)
|
||||||
has_results: defaultdict[str, set[str]] = defaultdict(set)
|
has_results: defaultdict[str, set[str]] = defaultdict(set)
|
||||||
|
|
||||||
|
# show cached feed list at startup
|
||||||
|
search.show_only_cache_entries()
|
||||||
|
search.on_resize()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
await _search_active.wait()
|
await _search_active.wait()
|
||||||
period = None
|
period = None
|
||||||
|
@ -742,7 +822,7 @@ async def fill_results(
|
||||||
pattern = await recv_chan.receive()
|
pattern = await recv_chan.receive()
|
||||||
|
|
||||||
period = time.time() - wait_start
|
period = time.time() - wait_start
|
||||||
print(f'{pattern} after {period}')
|
log.debug(f'{pattern} after {period}')
|
||||||
|
|
||||||
# during fast multiple key inputs, wait until a pause
|
# during fast multiple key inputs, wait until a pause
|
||||||
# (in typing) to initiate search
|
# (in typing) to initiate search
|
||||||
|
@ -841,8 +921,7 @@ async def handle_keyboard_input(
|
||||||
godwidget = search.godwidget
|
godwidget = search.godwidget
|
||||||
view = bar.view
|
view = bar.view
|
||||||
view.set_font_size(bar.dpi_font.px_size)
|
view.set_font_size(bar.dpi_font.px_size)
|
||||||
|
send, recv = trio.open_memory_channel(616)
|
||||||
send, recv = trio.open_memory_channel(16)
|
|
||||||
|
|
||||||
async with trio.open_nursery() as n:
|
async with trio.open_nursery() as n:
|
||||||
|
|
||||||
|
@ -857,6 +936,10 @@ async def handle_keyboard_input(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
bar.focus()
|
||||||
|
search.show_only_cache_entries()
|
||||||
|
await trio.sleep(0)
|
||||||
|
|
||||||
async for kbmsg in recv_chan:
|
async for kbmsg in recv_chan:
|
||||||
event, etype, key, mods, txt = kbmsg.to_tuple()
|
event, etype, key, mods, txt = kbmsg.to_tuple()
|
||||||
|
|
||||||
|
@ -867,10 +950,11 @@ async def handle_keyboard_input(
|
||||||
ctl = True
|
ctl = True
|
||||||
|
|
||||||
if key in (Qt.Key_Enter, Qt.Key_Return):
|
if key in (Qt.Key_Enter, Qt.Key_Return):
|
||||||
|
|
||||||
await search.chart_current_item(clear_to_cache=True)
|
|
||||||
_search_enabled = False
|
_search_enabled = False
|
||||||
continue
|
await search.chart_current_item(clear_to_cache=True)
|
||||||
|
search.show_only_cache_entries()
|
||||||
|
view.show_matches()
|
||||||
|
search.focus()
|
||||||
|
|
||||||
elif not ctl and not bar.text():
|
elif not ctl and not bar.text():
|
||||||
# if nothing in search text show the cache
|
# if nothing in search text show the cache
|
||||||
|
@ -887,7 +971,7 @@ async def handle_keyboard_input(
|
||||||
Qt.Key_Space, # i feel like this is the "native" one
|
Qt.Key_Space, # i feel like this is the "native" one
|
||||||
Qt.Key_Alt,
|
Qt.Key_Alt,
|
||||||
}:
|
}:
|
||||||
search.bar.unfocus()
|
bar.unfocus()
|
||||||
|
|
||||||
# kill the search and focus back on main chart
|
# kill the search and focus back on main chart
|
||||||
if godwidget:
|
if godwidget:
|
||||||
|
@ -935,9 +1019,10 @@ async def handle_keyboard_input(
|
||||||
if item:
|
if item:
|
||||||
parent_item = item.parent()
|
parent_item = item.parent()
|
||||||
|
|
||||||
|
# if we're in the cache section and thus the next
|
||||||
|
# selection is a cache item, switch and show it
|
||||||
|
# immediately since it should be very fast.
|
||||||
if parent_item and parent_item.text() == 'cache':
|
if parent_item and parent_item.text() == 'cache':
|
||||||
|
|
||||||
# if it's a cache item, switch and show it immediately
|
|
||||||
await search.chart_current_item(clear_to_cache=False)
|
await search.chart_current_item(clear_to_cache=False)
|
||||||
|
|
||||||
elif not ctl:
|
elif not ctl:
|
||||||
|
|
|
@ -21,7 +21,11 @@ Qt main window singletons and stuff.
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
import time
|
import time
|
||||||
from typing import Callable, Optional, Union
|
from typing import (
|
||||||
|
Callable,
|
||||||
|
Optional,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from pyqtgraph import QtGui
|
from pyqtgraph import QtGui
|
||||||
|
@ -30,6 +34,7 @@ from PyQt5.QtWidgets import QLabel, QStatusBar
|
||||||
|
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from ._style import _font_small, hcolor
|
from ._style import _font_small, hcolor
|
||||||
|
from ._chart import GodWidget
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
@ -153,7 +158,8 @@ class MainWindow(QtGui.QMainWindow):
|
||||||
# XXX: for tiling wms this should scale
|
# XXX: for tiling wms this should scale
|
||||||
# with the alloted window size.
|
# with the alloted window size.
|
||||||
# TODO: detect for tiling and if untrue set some size?
|
# TODO: detect for tiling and if untrue set some size?
|
||||||
size = (300, 500)
|
# size = (300, 500)
|
||||||
|
godwidget: GodWidget
|
||||||
|
|
||||||
title = 'piker chart (ur symbol is loading bby)'
|
title = 'piker chart (ur symbol is loading bby)'
|
||||||
|
|
||||||
|
@ -162,6 +168,9 @@ class MainWindow(QtGui.QMainWindow):
|
||||||
# self.setMinimumSize(*self.size)
|
# self.setMinimumSize(*self.size)
|
||||||
self.setWindowTitle(self.title)
|
self.setWindowTitle(self.title)
|
||||||
|
|
||||||
|
# set by runtime after `trio` is engaged.
|
||||||
|
self.godwidget: Optional[GodWidget] = None
|
||||||
|
|
||||||
self._status_bar: QStatusBar = None
|
self._status_bar: QStatusBar = None
|
||||||
self._status_label: QLabel = None
|
self._status_label: QLabel = None
|
||||||
self._size: Optional[tuple[int, int]] = None
|
self._size: Optional[tuple[int, int]] = None
|
||||||
|
@ -248,9 +257,10 @@ class MainWindow(QtGui.QMainWindow):
|
||||||
self.set_mode_name(name)
|
self.set_mode_name(name)
|
||||||
|
|
||||||
def current_screen(self) -> QtGui.QScreen:
|
def current_screen(self) -> QtGui.QScreen:
|
||||||
"""Get a frickin screen (if we can, gawd).
|
'''
|
||||||
|
Get a frickin screen (if we can, gawd).
|
||||||
|
|
||||||
"""
|
'''
|
||||||
app = QtGui.QApplication.instance()
|
app = QtGui.QApplication.instance()
|
||||||
|
|
||||||
for _ in range(3):
|
for _ in range(3):
|
||||||
|
@ -284,7 +294,7 @@ class MainWindow(QtGui.QMainWindow):
|
||||||
'''
|
'''
|
||||||
# https://stackoverflow.com/a/18975846
|
# https://stackoverflow.com/a/18975846
|
||||||
if not size and not self._size:
|
if not size and not self._size:
|
||||||
app = QtGui.QApplication.instance()
|
# app = QtGui.QApplication.instance()
|
||||||
geo = self.current_screen().geometry()
|
geo = self.current_screen().geometry()
|
||||||
h, w = geo.height(), geo.width()
|
h, w = geo.height(), geo.width()
|
||||||
# use approx 1/3 of the area of the screen by default
|
# use approx 1/3 of the area of the screen by default
|
||||||
|
@ -292,6 +302,33 @@ class MainWindow(QtGui.QMainWindow):
|
||||||
|
|
||||||
self.resize(*size or self._size)
|
self.resize(*size or self._size)
|
||||||
|
|
||||||
|
def resizeEvent(self, event: QtCore.QEvent) -> None:
|
||||||
|
if (
|
||||||
|
# event.spontaneous()
|
||||||
|
event.oldSize().height == event.size().height
|
||||||
|
):
|
||||||
|
event.ignore()
|
||||||
|
return
|
||||||
|
|
||||||
|
# XXX: uncomment for debugging..
|
||||||
|
# attrs = {}
|
||||||
|
# for key in dir(event):
|
||||||
|
# if key == '__dir__':
|
||||||
|
# continue
|
||||||
|
# attr = getattr(event, key)
|
||||||
|
# try:
|
||||||
|
# attrs[key] = attr()
|
||||||
|
# except TypeError:
|
||||||
|
# attrs[key] = attr
|
||||||
|
|
||||||
|
# from pprint import pformat
|
||||||
|
# print(
|
||||||
|
# f'{pformat(attrs)}\n'
|
||||||
|
# f'WINDOW RESIZE: {self.size()}\n\n'
|
||||||
|
# )
|
||||||
|
self.godwidget.on_win_resize(event)
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
|
||||||
# singleton app per actor
|
# singleton app per actor
|
||||||
_qt_win: QtGui.QMainWindow = None
|
_qt_win: QtGui.QMainWindow = None
|
||||||
|
|
|
@ -18,13 +18,19 @@
|
||||||
Chart trading, the only way to scalp.
|
Chart trading, the only way to scalp.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
import platform
|
import platform
|
||||||
import time
|
import time
|
||||||
from typing import Optional, Dict, Callable, Any
|
from typing import (
|
||||||
|
Optional,
|
||||||
|
Callable,
|
||||||
|
Any,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
)
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import tractor
|
import tractor
|
||||||
|
@ -60,6 +66,12 @@ from ..clearing._messages import (
|
||||||
from ._forms import open_form_input_handling
|
from ._forms import open_form_input_handling
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ._chart import (
|
||||||
|
ChartPlotWidget,
|
||||||
|
GodWidget,
|
||||||
|
)
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -73,10 +85,10 @@ class Dialog(Struct):
|
||||||
uuid: str
|
uuid: str
|
||||||
order: Order
|
order: Order
|
||||||
symbol: Symbol
|
symbol: Symbol
|
||||||
line: LevelLine
|
lines: list[LevelLine]
|
||||||
last_status_close: Callable = lambda: None
|
last_status_close: Callable = lambda: None
|
||||||
msgs: dict[str, dict] = {}
|
msgs: dict[str, dict] = {}
|
||||||
fills: Dict[str, Any] = {}
|
fills: dict[str, Any] = {}
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -100,8 +112,11 @@ class OrderMode:
|
||||||
mouse click and drag -> modify current order under cursor
|
mouse click and drag -> modify current order under cursor
|
||||||
|
|
||||||
'''
|
'''
|
||||||
chart: 'ChartPlotWidget' # type: ignore # noqa
|
godw: GodWidget
|
||||||
nursery: trio.Nursery
|
feed: Feed
|
||||||
|
chart: ChartPlotWidget # type: ignore # noqa
|
||||||
|
hist_chart: ChartPlotWidget # type: ignore # noqa
|
||||||
|
nursery: trio.Nursery # used by ``ui._position`` code?
|
||||||
quote_feed: Feed
|
quote_feed: Feed
|
||||||
book: OrderBook
|
book: OrderBook
|
||||||
lines: LineEditor
|
lines: LineEditor
|
||||||
|
@ -162,14 +177,15 @@ class OrderMode:
|
||||||
def line_from_order(
|
def line_from_order(
|
||||||
self,
|
self,
|
||||||
order: Order,
|
order: Order,
|
||||||
|
chart: Optional[ChartPlotWidget] = None,
|
||||||
**line_kwargs,
|
**line_kwargs,
|
||||||
|
|
||||||
) -> LevelLine:
|
) -> LevelLine:
|
||||||
|
|
||||||
level = order.price
|
level = order.price
|
||||||
line = order_line(
|
|
||||||
|
|
||||||
self.chart,
|
line = order_line(
|
||||||
|
chart or self.chart,
|
||||||
# TODO: convert these values into human-readable form
|
# TODO: convert these values into human-readable form
|
||||||
# (i.e. with k, m, M, B) type embedded suffixes
|
# (i.e. with k, m, M, B) type embedded suffixes
|
||||||
level=level,
|
level=level,
|
||||||
|
@ -211,24 +227,61 @@ class OrderMode:
|
||||||
|
|
||||||
return line
|
return line
|
||||||
|
|
||||||
|
def lines_from_order(
|
||||||
|
self,
|
||||||
|
order: Order,
|
||||||
|
**line_kwargs,
|
||||||
|
|
||||||
|
) -> list[LevelLine]:
|
||||||
|
|
||||||
|
lines: list[LevelLine] = []
|
||||||
|
for chart, kwargs in [
|
||||||
|
(self.chart, {}),
|
||||||
|
(self.hist_chart, {'only_show_markers_on_hover': True}),
|
||||||
|
]:
|
||||||
|
kwargs.update(line_kwargs)
|
||||||
|
line = self.line_from_order(
|
||||||
|
order=order,
|
||||||
|
chart=chart,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
lines.append(line)
|
||||||
|
|
||||||
|
return lines
|
||||||
|
|
||||||
def stage_order(
|
def stage_order(
|
||||||
self,
|
self,
|
||||||
|
|
||||||
action: str,
|
action: str,
|
||||||
trigger_type: str,
|
trigger_type: str,
|
||||||
|
|
||||||
) -> None:
|
) -> list[LevelLine]:
|
||||||
'''Stage an order for submission.
|
'''
|
||||||
|
Stage an order for submission by showing level lines and
|
||||||
|
configuring the order request message dynamically based on
|
||||||
|
allocator settings.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# not initialized yet
|
# not initialized yet
|
||||||
chart = self.chart
|
cursor = self.godw.get_cursor()
|
||||||
cursor = chart.linked.cursor
|
if not cursor:
|
||||||
if not (chart and cursor and cursor.active_plot):
|
return
|
||||||
|
|
||||||
|
chart = cursor.linked.chart
|
||||||
|
if (
|
||||||
|
not chart
|
||||||
|
and cursor
|
||||||
|
and cursor.active_plot
|
||||||
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
chart = cursor.active_plot
|
chart = cursor.active_plot
|
||||||
price = cursor._datum_xy[1]
|
price = cursor._datum_xy[1]
|
||||||
|
if not price:
|
||||||
|
# zero prices are not supported by any means
|
||||||
|
# since that's illogical / a no-op.
|
||||||
|
return
|
||||||
|
|
||||||
symbol = self.chart.linked.symbol
|
symbol = self.chart.linked.symbol
|
||||||
|
|
||||||
order = self._staged_order = Order(
|
order = self._staged_order = Order(
|
||||||
|
@ -242,27 +295,43 @@ class OrderMode:
|
||||||
exec_mode=trigger_type, # dark or live
|
exec_mode=trigger_type, # dark or live
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO: staged line mirroring? - need to keep track of multiple
|
||||||
|
# staged lines in editor - need to call
|
||||||
|
# `LineEditor.unstage_line()` on all staged lines..
|
||||||
|
# lines = self.lines_from_order(
|
||||||
|
|
||||||
line = self.line_from_order(
|
line = self.line_from_order(
|
||||||
order,
|
order,
|
||||||
|
chart=chart,
|
||||||
show_markers=True,
|
show_markers=True,
|
||||||
|
|
||||||
# just for the stage line to avoid
|
# just for the stage line to avoid
|
||||||
# flickering while moving the cursor
|
# flickering while moving the cursor
|
||||||
# around where it might trigger highlight
|
# around where it might trigger highlight
|
||||||
# then non-highlight depending on sensitivity
|
# then non-highlight depending on sensitivity
|
||||||
always_show_labels=True,
|
always_show_labels=True,
|
||||||
|
|
||||||
# don't highlight the "staging" line
|
# don't highlight the "staging" line
|
||||||
highlight_on_hover=False,
|
highlight_on_hover=False,
|
||||||
|
|
||||||
# prevent flickering of marker while moving/tracking cursor
|
# prevent flickering of marker while moving/tracking cursor
|
||||||
only_show_markers_on_hover=False,
|
only_show_markers_on_hover=False,
|
||||||
)
|
)
|
||||||
line = self.lines.stage_line(line)
|
self.lines.stage_line(line)
|
||||||
|
|
||||||
# hide crosshair y-line and label
|
|
||||||
cursor.hide_xhair()
|
|
||||||
|
|
||||||
# add line to cursor trackers
|
# add line to cursor trackers
|
||||||
cursor._trackers.add(line)
|
cursor._trackers.add(line)
|
||||||
|
|
||||||
|
# TODO: see above about mirroring.
|
||||||
|
# for line in lines:
|
||||||
|
# if line._chart is chart:
|
||||||
|
# self.lines.stage_line(line)
|
||||||
|
# cursor._trackers.add(line)
|
||||||
|
# break
|
||||||
|
|
||||||
|
# hide crosshair y-line and label
|
||||||
|
cursor.hide_xhair()
|
||||||
|
|
||||||
return line
|
return line
|
||||||
|
|
||||||
def submit_order(
|
def submit_order(
|
||||||
|
@ -285,13 +354,10 @@ class OrderMode:
|
||||||
|
|
||||||
order.symbol = order.symbol.front_fqsn()
|
order.symbol = order.symbol.front_fqsn()
|
||||||
|
|
||||||
line = self.line_from_order(
|
lines = self.lines_from_order(
|
||||||
order,
|
order,
|
||||||
|
|
||||||
show_markers=True,
|
show_markers=True,
|
||||||
only_show_markers_on_hover=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# register the "submitted" line under the cursor
|
# register the "submitted" line under the cursor
|
||||||
# to be displayed when above order ack arrives
|
# to be displayed when above order ack arrives
|
||||||
# (means the marker graphic doesn't show on screen until the
|
# (means the marker graphic doesn't show on screen until the
|
||||||
|
@ -302,8 +368,8 @@ class OrderMode:
|
||||||
# maybe place a grey line in "submission" mode
|
# maybe place a grey line in "submission" mode
|
||||||
# which will be updated to it's appropriate action
|
# which will be updated to it's appropriate action
|
||||||
# color once the submission ack arrives.
|
# color once the submission ack arrives.
|
||||||
self.lines.submit_line(
|
self.lines.submit_lines(
|
||||||
line=line,
|
lines=lines,
|
||||||
uuid=order.oid,
|
uuid=order.oid,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -311,21 +377,22 @@ class OrderMode:
|
||||||
uuid=order.oid,
|
uuid=order.oid,
|
||||||
order=order,
|
order=order,
|
||||||
symbol=order.symbol,
|
symbol=order.symbol,
|
||||||
line=line,
|
lines=lines,
|
||||||
last_status_close=self.multistatus.open_status(
|
last_status_close=self.multistatus.open_status(
|
||||||
f'submitting {order.exec_mode}-{order.action}',
|
f'submitting {order.exec_mode}-{order.action}',
|
||||||
final_msg=f'submitted {order.exec_mode}-{order.action}',
|
final_msg=f'submitted {order.exec_mode}-{order.action}',
|
||||||
clear_on_next=True,
|
clear_on_next=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: create a new ``OrderLine`` with this optional var defined
|
|
||||||
line.dialog = dialog
|
|
||||||
|
|
||||||
# enter submission which will be popped once a response
|
# enter submission which will be popped once a response
|
||||||
# from the EMS is received to move the order to a different# status
|
# from the EMS is received to move the order to a different# status
|
||||||
self.dialogs[order.oid] = dialog
|
self.dialogs[order.oid] = dialog
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
|
||||||
|
# TODO: create a new ``OrderLine`` with this optional var defined
|
||||||
|
line.dialog = dialog
|
||||||
|
|
||||||
# hook up mouse drag handlers
|
# hook up mouse drag handlers
|
||||||
line._on_drag_start = self.order_line_modify_start
|
line._on_drag_start = self.order_line_modify_start
|
||||||
line._on_drag_end = self.order_line_modify_complete
|
line._on_drag_end = self.order_line_modify_complete
|
||||||
|
@ -350,7 +417,7 @@ class OrderMode:
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
print(f'Line modify: {line}')
|
log.info(f'Order modify: {line}')
|
||||||
# cancel original order until new position is found?
|
# cancel original order until new position is found?
|
||||||
# TODO: make a config option for this behaviour..
|
# TODO: make a config option for this behaviour..
|
||||||
|
|
||||||
|
@ -361,8 +428,9 @@ class OrderMode:
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
level = line.value()
|
level = line.value()
|
||||||
# updateb by level change callback set in ``.line_from_order()``
|
# updated by level change callback set in ``.line_from_order()``
|
||||||
size = line.dialog.order.size
|
dialog = line.dialog
|
||||||
|
size = dialog.order.size
|
||||||
|
|
||||||
self.book.update(
|
self.book.update(
|
||||||
uuid=line.dialog.uuid,
|
uuid=line.dialog.uuid,
|
||||||
|
@ -370,8 +438,13 @@ class OrderMode:
|
||||||
size=size,
|
size=size,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ems response loop handlers
|
# adjust corresponding slow/fast chart line
|
||||||
|
# to match level
|
||||||
|
for ln in dialog.lines:
|
||||||
|
if ln is not line:
|
||||||
|
ln.set_level(line.value())
|
||||||
|
|
||||||
|
# EMS response msg handlers
|
||||||
def on_submit(
|
def on_submit(
|
||||||
self,
|
self,
|
||||||
uuid: str
|
uuid: str
|
||||||
|
@ -383,13 +456,18 @@ class OrderMode:
|
||||||
Commit the order line and registered order uuid, store ack time stamp.
|
Commit the order line and registered order uuid, store ack time stamp.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
line = self.lines.commit_line(uuid)
|
lines = self.lines.commit_line(uuid)
|
||||||
|
|
||||||
# a submission is the start of a new order dialog
|
# a submission is the start of a new order dialog
|
||||||
dialog = self.dialogs[uuid]
|
dialog = self.dialogs[uuid]
|
||||||
dialog.line = line
|
dialog.lines = lines
|
||||||
dialog.last_status_close()
|
dialog.last_status_close()
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
# hide any lines not currently moused-over
|
||||||
|
if not line.get_cursor():
|
||||||
|
line.hide_labels()
|
||||||
|
|
||||||
return dialog
|
return dialog
|
||||||
|
|
||||||
def on_fill(
|
def on_fill(
|
||||||
|
@ -415,17 +493,26 @@ class OrderMode:
|
||||||
|
|
||||||
'''
|
'''
|
||||||
dialog = self.dialogs[uuid]
|
dialog = self.dialogs[uuid]
|
||||||
line = dialog.line
|
lines = dialog.lines
|
||||||
if line:
|
# XXX: seems to fail on certain types of races?
|
||||||
|
# assert len(lines) == 2
|
||||||
|
if lines:
|
||||||
|
_, _, ratio = self.feed.get_ds_info()
|
||||||
|
for i, chart in [
|
||||||
|
(arrow_index, self.chart),
|
||||||
|
(self.feed.startup_hist_index + round(arrow_index/ratio),
|
||||||
|
self.hist_chart)
|
||||||
|
]:
|
||||||
self.arrows.add(
|
self.arrows.add(
|
||||||
|
chart.plotItem,
|
||||||
uuid,
|
uuid,
|
||||||
arrow_index,
|
i,
|
||||||
price,
|
price,
|
||||||
pointing=pointing,
|
pointing=pointing,
|
||||||
color=line.color
|
color=lines[0].color
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
log.warn("No line for order {uuid}!?")
|
log.warn("No line(s) for order {uuid}!?")
|
||||||
|
|
||||||
async def on_exec(
|
async def on_exec(
|
||||||
self,
|
self,
|
||||||
|
@ -486,7 +573,8 @@ class OrderMode:
|
||||||
)
|
)
|
||||||
|
|
||||||
def cancel_all_orders(self) -> list[str]:
|
def cancel_all_orders(self) -> list[str]:
|
||||||
'''Cancel all orders for the current chart.
|
'''
|
||||||
|
Cancel all orders for the current chart.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
return self.cancel_orders_from_lines(
|
return self.cancel_orders_from_lines(
|
||||||
|
@ -568,7 +656,7 @@ class OrderMode:
|
||||||
async def open_order_mode(
|
async def open_order_mode(
|
||||||
|
|
||||||
feed: Feed,
|
feed: Feed,
|
||||||
chart: 'ChartPlotWidget', # noqa
|
godw: GodWidget,
|
||||||
fqsn: str,
|
fqsn: str,
|
||||||
started: trio.Event,
|
started: trio.Event,
|
||||||
|
|
||||||
|
@ -581,6 +669,9 @@ async def open_order_mode(
|
||||||
state, mostly graphics / UI.
|
state, mostly graphics / UI.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
chart = godw.rt_linked.chart
|
||||||
|
hist_chart = godw.hist_linked.chart
|
||||||
|
|
||||||
multistatus = chart.window().status_bar
|
multistatus = chart.window().status_bar
|
||||||
done = multistatus.open_status('starting order mode..')
|
done = multistatus.open_status('starting order mode..')
|
||||||
|
|
||||||
|
@ -606,11 +697,10 @@ async def open_order_mode(
|
||||||
|
|
||||||
):
|
):
|
||||||
log.info(f'Opening order mode for {fqsn}')
|
log.info(f'Opening order mode for {fqsn}')
|
||||||
view = chart.view
|
|
||||||
|
|
||||||
# annotations editors
|
# annotations editors
|
||||||
lines = LineEditor(chart=chart)
|
lines = LineEditor(godw=godw)
|
||||||
arrows = ArrowEditor(chart, {})
|
arrows = ArrowEditor(godw=godw)
|
||||||
|
|
||||||
# symbol id
|
# symbol id
|
||||||
symbol = chart.linked.symbol
|
symbol = chart.linked.symbol
|
||||||
|
@ -663,11 +753,11 @@ async def open_order_mode(
|
||||||
)
|
)
|
||||||
|
|
||||||
pp_tracker = PositionTracker(
|
pp_tracker = PositionTracker(
|
||||||
chart,
|
[chart, hist_chart],
|
||||||
alloc,
|
alloc,
|
||||||
startup_pp
|
startup_pp
|
||||||
)
|
)
|
||||||
pp_tracker.hide()
|
pp_tracker.nav.hide()
|
||||||
trackers[account_name] = pp_tracker
|
trackers[account_name] = pp_tracker
|
||||||
|
|
||||||
assert pp_tracker.startup_pp.size == pp_tracker.live_pp.size
|
assert pp_tracker.startup_pp.size == pp_tracker.live_pp.size
|
||||||
|
@ -679,8 +769,8 @@ async def open_order_mode(
|
||||||
|
|
||||||
# on existing position, show pp tracking graphics
|
# on existing position, show pp tracking graphics
|
||||||
if pp_tracker.startup_pp.size != 0:
|
if pp_tracker.startup_pp.size != 0:
|
||||||
pp_tracker.show()
|
pp_tracker.nav.show()
|
||||||
pp_tracker.hide_info()
|
pp_tracker.nav.hide_info()
|
||||||
|
|
||||||
# setup order mode sidepane widgets
|
# setup order mode sidepane widgets
|
||||||
form: FieldsForm = chart.sidepane
|
form: FieldsForm = chart.sidepane
|
||||||
|
@ -720,7 +810,10 @@ async def open_order_mode(
|
||||||
# top level abstraction which wraps all this crazyness into
|
# top level abstraction which wraps all this crazyness into
|
||||||
# a namespace..
|
# a namespace..
|
||||||
mode = OrderMode(
|
mode = OrderMode(
|
||||||
|
godw,
|
||||||
|
feed,
|
||||||
chart,
|
chart,
|
||||||
|
hist_chart,
|
||||||
tn,
|
tn,
|
||||||
feed,
|
feed,
|
||||||
book,
|
book,
|
||||||
|
@ -737,8 +830,8 @@ async def open_order_mode(
|
||||||
# select a pp to track
|
# select a pp to track
|
||||||
tracker: PositionTracker = trackers[pp_account]
|
tracker: PositionTracker = trackers[pp_account]
|
||||||
mode.current_pp = tracker
|
mode.current_pp = tracker
|
||||||
tracker.show()
|
tracker.nav.show()
|
||||||
tracker.hide_info()
|
tracker.nav.hide_info()
|
||||||
|
|
||||||
# XXX: would love to not have to do this separate from edit
|
# XXX: would love to not have to do this separate from edit
|
||||||
# fields (which are done in an async loop - see below)
|
# fields (which are done in an async loop - see below)
|
||||||
|
@ -754,13 +847,13 @@ async def open_order_mode(
|
||||||
)
|
)
|
||||||
|
|
||||||
# make fill bar and positioning snapshot
|
# make fill bar and positioning snapshot
|
||||||
order_pane.on_ui_settings_change('limit', tracker.alloc.limit())
|
order_pane.update_status_ui(tracker)
|
||||||
order_pane.update_status_ui(pp=tracker)
|
|
||||||
|
|
||||||
# TODO: create a mode "manager" of sorts?
|
# TODO: create a mode "manager" of sorts?
|
||||||
# -> probably just call it "UxModes" err sumthin?
|
# -> probably just call it "UxModes" err sumthin?
|
||||||
# so that view handlers can access it
|
# so that view handlers can access it
|
||||||
view.order_mode = mode
|
chart.view.order_mode = mode
|
||||||
|
hist_chart.view.order_mode = mode
|
||||||
|
|
||||||
order_pane.on_ui_settings_change('account', pp_account)
|
order_pane.on_ui_settings_change('account', pp_account)
|
||||||
mode.pane.display_pnl(mode.current_pp)
|
mode.pane.display_pnl(mode.current_pp)
|
||||||
|
@ -785,6 +878,7 @@ async def open_order_mode(
|
||||||
|
|
||||||
# ``ChartView`` input async handler startup
|
# ``ChartView`` input async handler startup
|
||||||
chart.view.open_async_input_handler(),
|
chart.view.open_async_input_handler(),
|
||||||
|
hist_chart.view.open_async_input_handler(),
|
||||||
|
|
||||||
# pp pane kb inputs
|
# pp pane kb inputs
|
||||||
open_form_input_handling(
|
open_form_input_handling(
|
||||||
|
|
Loading…
Reference in New Issue