Factor pos tracker UI element mgmt into new type

More or less moves all the UI related position "nav" logic and graphics
item management into a new `._position.Nav` composite type + api for
high level mgmt of position graphics indicators across multiple charts
(fast and slow).
history_view
Tyler Goodlet 2022-09-07 15:50:03 -04:00
parent 8f2823d5f0
commit a786df65de
1 changed files with 280 additions and 195 deletions

View File

@ -24,6 +24,7 @@ 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 ( from typing import (
Callable,
Optional, Optional,
TYPE_CHECKING, TYPE_CHECKING,
) )
@ -44,6 +45,7 @@ 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
@ -203,7 +205,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
@ -213,7 +215,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}'
) )
@ -224,8 +226,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)
@ -476,6 +478,221 @@ _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)
if size > 0:
style = '|<'
elif size < 0:
style = '>|'
arrow = LevelMarker(
chart=chart,
style=style,
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 = position_line(
chart=chart,
level=price,
size=size,
color=self.color,
marker=arrow,
)
self.lines[key] = line
# modify existing indicator line
else:
line.set_level(price)
level_marker.level = price
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': account,
})
line.show()
# remove line from view for a net-zero pos
elif line:
line.delete()
self.lines[key] = None
# label updates
size_label = self.size_labels[key]
size_label.fields['slots_used'] = slots_used
size_label.render()
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():
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 charts.
'''
for (
pp_label,
size_label,
line,
level_marker,
) in self.iter_ui_elements():
# labels
level_marker.show()
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: MEGALOLZ - this will cause the ui to hannngggg!!!?!?!?!
# 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 symbol
@ -486,75 +703,80 @@ class PositionTracker:
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)
view = chart.getViewBox() # TODO: maybe add this as a method ``Nav.add_chart()``
# init all UI elements
for key, chart in nav.charts.items():
view = chart.getViewBox()
# literally the 'pp' (pee pee) label that's always in view # literally the 'pp' (pee pee) "position price" label that's
self.pp_label = pp_label = Label( # always in view
view=view, pp_label = Label(
fmt_str='pp', view=view,
color=self._color, fmt_str='pp',
update_on_range_change=False, color=nav.color,
) update_on_range_change=False,
)
# if nav._level_marker:
# nav._level_marker.delete()
# create placeholder 'up' level arrow arrow = mk_level_marker(
self._level_marker = None chart=chart,
self._level_marker = self.level_marker(size=1) size=1,
level=nav.level,
on_paint=nav.update_graphics,
)
pp_label.scene_anchor = partial( view.scene().addItem(arrow)
gpath_pin, nav.level_markers[key] = arrow
gpath=self._level_marker,
label=pp_label,
)
pp_label.render()
self.size_label = size_label = Label( pp_label.scene_anchor = partial(
view=view, gpath_pin,
color=self._color, gpath=arrow,
label=pp_label,
)
pp_label.render()
nav.pp_labels[key] = pp_label
# this is "static" label size_label = Label(
# update_on_range_change=False, view=view,
fmt_str='\n'.join(( color=self.nav.color,
':{slots_used:.1f}x',
)),
fields={ # this is "static" label
'slots_used': 0, # update_on_range_change=False,
}, fmt_str='\n'.join((
) ':{slots_used:.1f}x',
size_label.render() )),
size_label.scene_anchor = partial( fields={
pp_tight_and_right, 'slots_used': 0,
label=self.pp_label, },
) )
size_label.render()
size_label.scene_anchor = partial(
pp_tight_and_right,
label=pp_label,
)
nav.size_labels[key] = size_label
nav.show()
@property @property
def pane(self) -> FieldsForm: def pane(self) -> FieldsForm:
@ -564,21 +786,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,
@ -631,142 +838,20 @@ 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
self.size_label.fields['slots_used'] = round(
alloc.slots_used(pp), ndigits=1)
self.size_label.render()
if pp.size == 0: if pp.size == 0:
self.hide() self.nav.hide()
else: else:
self._level_marker.level = pp.ppu if self.live_pp.size:
self.nav.show()
# 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