Compare commits

..

10 Commits

Author SHA1 Message Date
Tyler Goodlet c58a56fca7 Comment stream drain idea
Likely it's not going to be that useful but keep it for reference for
the moment.
2022-01-25 08:11:20 -05:00
Tyler Goodlet 719187bf5a WIP idea: drain feed stream, doesn't do much.. 2022-01-25 08:11:06 -05:00
Tyler Goodlet c7436d5857 Drop dpi logging back to debug 2022-01-25 08:08:50 -05:00
Tyler Goodlet 82a9c62c07 Annoying doc string(s) 2022-01-25 08:00:45 -05:00
Tyler Goodlet edd227228c Fix bottom axis check logic for overlays, try out some px perfection 2022-01-25 08:00:45 -05:00
Tyler Goodlet 00b1b2a10c Allow passing in parent to `Label` 2022-01-25 08:00:45 -05:00
Tyler Goodlet 1a077c0553 Hide the unit vlm after the $vlm is up
Since more curves costs more processing and since the vlm and $vlm
curves are normally very close to the same (graphically) we hide the
unit volume curve once the dollar volume is up (after the fsp daemon-task is
spawned) and just expect the user to understand the diff in axes units.
Also, use the new `title=` api to `.overlay_plotitem()`.
2022-01-25 08:00:45 -05:00
Tyler Goodlet 26327e5462 Use overlay api to access multi-axes by name 2022-01-25 08:00:45 -05:00
Tyler Goodlet d600a2ca70 Make axes labels more pixel perfect 2022-01-25 08:00:45 -05:00
Tyler Goodlet e4bf3a5fe4 Pop vlm chart from subplots to avoid double render 2022-01-25 08:00:45 -05:00
9 changed files with 157 additions and 97 deletions

View File

@ -1,5 +1,5 @@
# piker: trading gear for hackers # piker: trading gear for hackers
# Copyright (C) 2018-present Tyler Goodlet (in stewardship of piker0) # Copyright (C) Tyler Goodlet (in stewardship of pikers)
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by # it under the terms of the GNU Affero General Public License as published by

View File

@ -287,6 +287,8 @@ class AxisLabel(pg.GraphicsObject):
self.path = None self.path = None
self.rect = None self.rect = None
self._pw = self.pixelWidth()
def paint( def paint(
self, self,
p: QtGui.QPainter, p: QtGui.QPainter,
@ -419,6 +421,7 @@ class XAxisLabel(AxisLabel):
abs_pos: QPointF, # scene coords abs_pos: QPointF, # scene coords
value: float, # data for text value: float, # data for text
offset: int = 0 # if have margins, k? offset: int = 0 # if have margins, k?
) -> None: ) -> None:
timestrs = self._parent._indexes_to_timestrs([int(value)]) timestrs = self._parent._indexes_to_timestrs([int(value)])
@ -433,17 +436,19 @@ class XAxisLabel(AxisLabel):
w = self.boundingRect().width() w = self.boundingRect().width()
self.setPos(QPointF( self.setPos(
abs_pos.x() - w/2, QPointF(
abs_pos.x() - w/2 - self._pw,
y_offset/2, y_offset/2,
)) )
)
self.update() self.update()
def _draw_arrow_path(self): def _draw_arrow_path(self):
y_offset = self._parent.style['tickTextOffset'][1] y_offset = self._parent.style['tickTextOffset'][1]
path = QtGui.QPainterPath() path = QtGui.QPainterPath()
h, w = self.rect.height(), self.rect.width() h, w = self.rect.height(), self.rect.width()
middle = w/2 - 0.5 middle = w/2 - self._pw * 0.5
aw = h/2 aw = h/2
left = middle - aw left = middle - aw
right = middle + aw right = middle + aw
@ -513,10 +518,12 @@ class YAxisLabel(AxisLabel):
br = self.boundingRect() br = self.boundingRect()
h = br.height() h = br.height()
self.setPos(QPointF( self.setPos(
QPointF(
x_offset, x_offset,
abs_pos.y() - h / 2 - self._y_margin / 2 abs_pos.y() - h / 2 - self._pw,
)) )
)
self.update() self.update()
def update_on_resize(self, vr, r): def update_on_resize(self, vr, r):
@ -553,7 +560,7 @@ class YAxisLabel(AxisLabel):
path = QtGui.QPainterPath() path = QtGui.QPainterPath()
h = self.rect.height() h = self.rect.height()
path.moveTo(0, 0) path.moveTo(0, 0)
path.lineTo(-x_offset - h/4, h/2.) path.lineTo(-x_offset - h/4, h/2. - self._pw/2)
path.lineTo(0, h) path.lineTo(0, h)
path.closeSubpath() path.closeSubpath()
self.path = path self.path = path

View File

@ -479,14 +479,20 @@ class LinkedSplits(QWidget):
axisItems=axes, axisItems=axes,
**cpw_kwargs, **cpw_kwargs,
) )
cpw.hideAxis('left')
cpw.hideAxis('bottom')
if self.xaxis_chart: if self.xaxis_chart:
self.xaxis_chart.hideAxis('bottom')
# presuming we only want it at the true bottom of all charts. # presuming we only want it at the true bottom of all charts.
# XXX: uses new api from our ``pyqtgraph`` fork. # XXX: uses new api from our ``pyqtgraph`` fork.
# https://github.com/pikers/pyqtgraph/tree/plotitemoverlay_onto_pg_master # https://github.com/pikers/pyqtgraph/tree/plotitemoverlay_onto_pg_master
_ = self.xaxis_chart.removeAxis('bottom', unlink=False) # _ = self.xaxis_chart.removeAxis('bottom', unlink=False)
assert 'bottom' not in self.xaxis_chart.plotItem.axes # assert 'bottom' not in self.xaxis_chart.plotItem.axes
self.xaxis_chart = cpw self.xaxis_chart = cpw
cpw.showAxis('bottom')
if self.xaxis_chart is None: if self.xaxis_chart is None:
self.xaxis_chart = cpw self.xaxis_chart = cpw
@ -726,11 +732,6 @@ class ChartPlotWidget(pg.PlotWidget):
self._static_yrange = static_yrange # for "known y-range style" self._static_yrange = static_yrange # for "known y-range style"
self._view_mode: str = 'follow' self._view_mode: str = 'follow'
# show only right side axes
self.hideAxis('left')
self.showAxis('right')
# self.showAxis('left')
# show background grid # show background grid
self.showGrid(x=False, y=True, alpha=0.3) self.showGrid(x=False, y=True, alpha=0.3)
@ -862,55 +863,58 @@ class ChartPlotWidget(pg.PlotWidget):
def overlay_plotitem( def overlay_plotitem(
self, self,
name: str, name: str,
index: Optional[int] = None,
axis_title: Optional[str] = None,
axis_side: str = 'right',
axis_kwargs: dict = {}, axis_kwargs: dict = {},
) -> pg.PlotItem: ) -> pg.PlotItem:
# Custom viewbox impl # Custom viewbox impl
cv = self.mk_vb(name) cv = self.mk_vb(name)
cv.chart = self cv.chart = self
# xaxis = DynamicDateAxis( allowed_sides = {'left', 'right'}
# orientation='bottom', if axis_side not in allowed_sides:
# linkedsplits=self.linked, raise ValueError(f'``axis_side``` must be in {allowed_sides}')
# )
yaxis = PriceAxis( yaxis = PriceAxis(
orientation='right', orientation=axis_side,
linkedsplits=self.linked, linkedsplits=self.linked,
**axis_kwargs, **axis_kwargs,
) )
plotitem = pg.PlotItem( pi = pg.PlotItem(
parent=self.plotItem, parent=self.plotItem,
name=name, name=name,
enableMenu=False, enableMenu=False,
viewBox=cv, viewBox=cv,
axisItems={ axisItems={
# 'bottom': xaxis, # 'bottom': xaxis,
'right': yaxis, axis_side: yaxis,
}, },
default_axes=[], default_axes=[],
) )
# plotitem.setAxisItems( pi.hideButtons()
# add_to_layout=False,
# axisItems={
# 'bottom': xaxis,
# 'right': yaxis,
# },
# )
# plotite.hideAxis('right')
# plotite.hideAxis('bottom')
# plotitem.addItem(curve)
cv.enable_auto_yrange() cv.enable_auto_yrange()
# plotitem.enableAutoRange(axis='y') # compose this new plot's graphics with the current chart's
plotitem.hideButtons() # existing one but with separate axes as neede and specified.
self.pi_overlay.add_plotitem( self.pi_overlay.add_plotitem(
plotitem, pi,
index=index,
# only link x-axes, # only link x-axes,
link_axes=(0,), link_axes=(0,),
) )
return plotitem
# add axis title
# TODO: do we want this API to still work?
# raxis = pi.getAxis('right')
axis = self.pi_overlay.get_axis(pi, axis_side)
axis.set_title(axis_title or name, view=pi.getViewBox())
return pi
def draw_curve( def draw_curve(
self, self,
@ -1016,7 +1020,8 @@ class ChartPlotWidget(pg.PlotWidget):
# add y-axis "last" value label # add y-axis "last" value label
last = self._ysticks[name] = YAxisLabel( last = self._ysticks[name] = YAxisLabel(
chart=self, chart=self,
parent=self.getAxis('right'), # parent=self.getAxis('right'),
parent=self.pi_overlay.get_axis(self.plotItem, 'right'),
# TODO: pass this from symbol data # TODO: pass this from symbol data
digits=digits, digits=digits,
opacity=1, opacity=1,

View File

@ -369,7 +369,13 @@ class Cursor(pg.GraphicsObject):
self, self,
plot: 'ChartPlotWidget', # noqa plot: 'ChartPlotWidget', # noqa
digits: int = 0, digits: int = 0,
) -> None: ) -> None:
'''
Add chart to tracked set such that a cross-hair and possibly
curve tracking cursor can be drawn on the plot.
'''
# add ``pg.graphicsItems.InfiniteLine``s # add ``pg.graphicsItems.InfiniteLine``s
# vertical and horizonal lines and a y-axis label # vertical and horizonal lines and a y-axis label
@ -382,7 +388,8 @@ class Cursor(pg.GraphicsObject):
yl = YAxisLabel( yl = YAxisLabel(
chart=plot, chart=plot,
parent=plot.getAxis('right'), # parent=plot.getAxis('right'),
parent=plot.pi_overlay.get_axis(plot.plotItem, 'right'),
digits=digits or self.digits, digits=digits or self.digits,
opacity=_ch_label_opac, opacity=_ch_label_opac,
bg_color=self.label_color, bg_color=self.label_color,
@ -424,19 +431,25 @@ class Cursor(pg.GraphicsObject):
# ONLY create an x-axis label for the cursor # ONLY create an x-axis label for the cursor
# if this plot owns the 'bottom' axis. # if this plot owns the 'bottom' axis.
if 'bottom' in plot.plotItem.axes: # if 'bottom' in plot.plotItem.axes:
self.xaxis_label = XAxisLabel( if plot.linked.xaxis_chart is plot:
xlabel = self.xaxis_label = XAxisLabel(
parent=self.plots[plot_index].getAxis('bottom'), parent=self.plots[plot_index].getAxis('bottom'),
# parent=self.plots[plot_index].pi_overlay.get_axis(plot.plotItem, 'bottom'),
opacity=_ch_label_opac, opacity=_ch_label_opac,
bg_color=self.label_color, bg_color=self.label_color,
) )
# place label off-screen during startup # place label off-screen during startup
self.xaxis_label.setPos(self.plots[0].mapFromView(QPointF(0, 0))) xlabel.setPos(
self.plots[0].mapFromView(QPointF(0, 0))
)
xlabel.show()
def add_curve_cursor( def add_curve_cursor(
self, self,
plot: 'ChartPlotWidget', # noqa plot: 'ChartPlotWidget', # noqa
curve: 'PlotCurveItem', # noqa curve: 'PlotCurveItem', # noqa
) -> LineDot: ) -> LineDot:
# if this plot contains curves add line dot "cursors" to denote # if this plot contains curves add line dot "cursors" to denote
# the current sample under the mouse # the current sample under the mouse
@ -493,24 +506,27 @@ class Cursor(pg.GraphicsObject):
ix = round(x) # since bars are centered around index ix = round(x) # since bars are centered around index
# px perfect...
line_offset = self._lw / 2
# round y value to nearest tick step # round y value to nearest tick step
m = self._y_incr_mult m = self._y_incr_mult
iy = round(y * m) / m iy = round(y * m) / m
vl_y = iy - line_offset
# px perfect...
line_offset = self._lw / 2
# update y-range items # update y-range items
if iy != last_iy: if iy != last_iy:
if self._y_label_update: if self._y_label_update:
self.graphics[self.active_plot]['yl'].update_label( self.graphics[self.active_plot]['yl'].update_label(
abs_pos=plot.mapFromView(QPointF(ix, iy)), # abs_pos=plot.mapFromView(QPointF(ix, iy)),
abs_pos=plot.mapFromView(QPointF(ix, vl_y)),
value=iy value=iy
) )
# only update horizontal xhair line if label is enabled # only update horizontal xhair line if label is enabled
self.graphics[plot]['hl'].setY(iy) # self.graphics[plot]['hl'].setY(iy)
self.graphics[plot]['hl'].setY(vl_y)
# update all trackers # update all trackers
for item in self._trackers: for item in self._trackers:
@ -541,9 +557,6 @@ class Cursor(pg.GraphicsObject):
# left axis offset width for calcuating # left axis offset width for calcuating
# absolute x-axis label placement. # absolute x-axis label placement.
left_axis_width = 0 left_axis_width = 0
if 'bottom' in axes:
left = axes.get('left') left = axes.get('left')
if left: if left:
left_axis_width = left['item'].width() left_axis_width = left['item'].width()

View File

@ -115,13 +115,14 @@ async def update_linked_charts_graphics(
vlm_chart: Optional[ChartPlotWidget] = None, vlm_chart: Optional[ChartPlotWidget] = None,
) -> None: ) -> None:
'''The 'main' (price) chart real-time update loop. '''
The 'main' (price) chart real-time update loop.
Receive from the primary instrument quote stream and update the OHLC Receive from the primary instrument quote stream and update the OHLC
chart. chart.
''' '''
# TODO: bunch of stuff: # TODO: bunch of stuff (some might be done already, can't member):
# - I'm starting to think all this logic should be # - I'm starting to think all this logic should be
# done in one place and "graphics update routines" # done in one place and "graphics update routines"
# should not be doing any length checking and array diffing. # should not be doing any length checking and array diffing.
@ -181,13 +182,34 @@ async def update_linked_charts_graphics(
view = chart.view view = chart.view
last_quote = time.time() last_quote = time.time()
# async def iter_drain_quotes():
# # NOTE: all code below this loop is expected to be synchronous
# # and thus draw instructions are not picked up jntil the next
# # wait / iteration.
# async for quotes in stream:
# while True:
# try:
# moar = stream.receive_nowait()
# except trio.WouldBlock:
# yield quotes
# break
# else:
# for sym, quote in moar.items():
# ticks_frame = quote.get('ticks')
# if ticks_frame:
# quotes[sym].setdefault(
# 'ticks', []).extend(ticks_frame)
# print('pulled extra')
# yield quotes
# async for quotes in iter_drain_quotes():
async for quotes in stream: async for quotes in stream:
now = time.time()
quote_period = time.time() - last_quote quote_period = time.time() - last_quote
quote_rate = round( quote_rate = round(
1/quote_period, 1) if quote_period > 0 else float('inf') 1/quote_period, 1) if quote_period > 0 else float('inf')
if ( if (
quote_period <= 1/_quote_throttle_rate quote_period <= 1/_quote_throttle_rate
@ -196,7 +218,8 @@ async def update_linked_charts_graphics(
and quote_rate >= _quote_throttle_rate * 1.5 and quote_rate >= _quote_throttle_rate * 1.5
): ):
log.warning(f'High quote rate {symbol.key}: {quote_rate}') log.warning(f'High quote rate {symbol.key}: {quote_rate}')
last_quote = now
last_quote = time.time()
# 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():
@ -621,9 +644,15 @@ async def display_symbol_data(
await trio.sleep(0) await trio.sleep(0)
linkedsplits.resize_sidepanes() linkedsplits.resize_sidepanes()
# NOTE: we pop the volume chart from the subplots set so
# that it isn't double rendered in the display loop
# above since we do a maxmin calc on the volume data to
# determine if auto-range adjustements should be made.
linkedsplits.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]()
# let the app run. # let the app run.. bby
await trio.sleep_forever() await trio.sleep_forever()

View File

@ -655,7 +655,7 @@ async def open_vlm_displays(
last_val_sticky.update_from_data(-1, value) last_val_sticky.update_from_data(-1, value)
chart.update_curve_from_array( vlm_curve = chart.update_curve_from_array(
'volume', 'volume',
shm.array, shm.array,
) )
@ -690,10 +690,11 @@ async def open_vlm_displays(
pi = chart.overlay_plotitem( pi = chart.overlay_plotitem(
'dolla_vlm', 'dolla_vlm',
index=0, # place axis on inside (nearest to chart)
axis_title=' $vlm',
axis_side='right',
axis_kwargs={ axis_kwargs={
# 'humanize': True, 'typical_max_str': ' 100.0 M ',
# 'text': 'dvlm',
'typical_max_str': ' 99.9 M ',
'formatter': partial( 'formatter': partial(
humanize, humanize,
digits=2, digits=2,
@ -702,10 +703,6 @@ async def open_vlm_displays(
) )
# add axis title
raxis = pi.getAxis('right')
raxis.set_title(' $vlm', view=pi.getViewBox())
# add custom auto range handler # add custom auto range handler
pi.vb._maxmin = partial(maxmin, name='dolla_vlm') pi.vb._maxmin = partial(maxmin, name='dolla_vlm')
@ -716,13 +713,18 @@ async def open_vlm_displays(
array_key='dolla_vlm', array_key='dolla_vlm',
overlay=pi, overlay=pi,
color='charcoal', # color='bracket',
# TODO: this color or dark volume
# color='charcoal',
step_mode=True, step_mode=True,
# **conf.get('chart_kwargs', {}) # **conf.get('chart_kwargs', {})
) )
# TODO: is there a way to "sync" the dual axes such that only # TODO: is there a way to "sync" the dual axes such that only
# one curve is needed? # one curve is needed?
# curve.hide() # hide the original vlm curve since the $vlm one is now
# displayed and the curves are effectively the same minus
# liquidity events (well at least on low OHLC periods - 1s).
vlm_curve.hide()
# TODO: we need a better API to do this.. # TODO: we need a better API to do this..
# specially store ref to shm for lookup in display loop # specially store ref to shm for lookup in display loop

View File

@ -34,7 +34,7 @@ from ._style import (
class Label: class Label:
""" '''
A plain ol' "scene label" using an underlying ``QGraphicsTextItem``. A plain ol' "scene label" using an underlying ``QGraphicsTextItem``.
After hacking for many days on multiple "label" systems inside After hacking for many days on multiple "label" systems inside
@ -50,10 +50,8 @@ class Label:
small, re-usable label components that can actually be used to build small, re-usable label components that can actually be used to build
production grade UIs... production grade UIs...
""" '''
def __init__( def __init__(
self, self,
view: pg.ViewBox, view: pg.ViewBox,
fmt_str: str, fmt_str: str,
@ -63,6 +61,7 @@ class Label:
font_size: str = 'small', font_size: str = 'small',
opacity: float = 1, opacity: float = 1,
fields: dict = {}, fields: dict = {},
parent: pg.GraphicsObject = None,
update_on_range_change: bool = True, update_on_range_change: bool = True,
) -> None: ) -> None:
@ -71,11 +70,13 @@ class Label:
self._fmt_str = fmt_str self._fmt_str = fmt_str
self._view_xy = QPointF(0, 0) self._view_xy = QPointF(0, 0)
self.scene_anchor: Optional[Callable[..., QPointF]] = None self.scene_anchor: Optional[
Callable[..., QPointF]
] = None
self._x_offset = x_offset self._x_offset = x_offset
txt = self.txt = QtWidgets.QGraphicsTextItem() txt = self.txt = QtWidgets.QGraphicsTextItem(parent=parent)
txt.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) txt.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
vb.scene().addItem(txt) vb.scene().addItem(txt)
@ -86,7 +87,6 @@ class Label:
) )
dpi_font.configure_to_dpi() dpi_font.configure_to_dpi()
txt.setFont(dpi_font.font) txt.setFont(dpi_font.font)
txt.setOpacity(opacity) txt.setOpacity(opacity)
# register viewbox callbacks # register viewbox callbacks
@ -109,7 +109,7 @@ class Label:
# self.setTextInteractionFlags(QtGui.Qt.TextEditorInteraction) # self.setTextInteractionFlags(QtGui.Qt.TextEditorInteraction)
@property @property
def color(self): def color(self) -> str:
return self._hcolor return self._hcolor
@color.setter @color.setter
@ -118,9 +118,10 @@ class Label:
self._hcolor = color self._hcolor = color
def update(self) -> None: def update(self) -> None:
'''Update this label either by invoking its '''
user defined anchoring function, or by positioning Update this label either by invoking its user defined anchoring
to the last recorded data view coordinates. function, or by positioning to the last recorded data view
coordinates.
''' '''
# move label in scene coords to desired position # move label in scene coords to desired position
@ -234,7 +235,8 @@ class Label:
class FormatLabel(QLabel): class FormatLabel(QLabel):
'''Kinda similar to above but using the widget apis. '''
Kinda similar to above but using the widget apis.
''' '''
def __init__( def __init__(
@ -273,8 +275,8 @@ class FormatLabel(QLabel):
QSizePolicy.Expanding, QSizePolicy.Expanding,
QSizePolicy.Expanding, QSizePolicy.Expanding,
) )
self.setAlignment(Qt.AlignVCenter self.setAlignment(
| Qt.AlignLeft Qt.AlignVCenter | Qt.AlignLeft
) )
self.setText(self.fmt_str) self.setText(self.fmt_str)

View File

@ -334,10 +334,11 @@ class LevelLine(pg.InfiniteLine):
w: QtWidgets.QWidget w: QtWidgets.QWidget
) -> None: ) -> None:
"""Core paint which we override (yet again) '''
Core paint which we override (yet again)
from pg.. from pg..
""" '''
p.setRenderHint(p.Antialiasing) p.setRenderHint(p.Antialiasing)
# these are in viewbox coords # these are in viewbox coords

View File

@ -16,6 +16,7 @@
""" """
Qt UI styling. Qt UI styling.
""" """
from typing import Optional, Dict from typing import Optional, Dict
import math import math
@ -141,7 +142,7 @@ class DpiAwareFont:
self._font_inches = inches self._font_inches = inches
font_size = math.floor(inches * dpi) font_size = math.floor(inches * dpi)
log.info( log.debug(
f"screen:{screen.name()}]\n" f"screen:{screen.name()}]\n"
f"pDPI: {pdpi}, lDPI: {ldpi}, scale: {scale}\n" f"pDPI: {pdpi}, lDPI: {ldpi}, scale: {scale}\n"
f"\nOur best guess font size is {font_size}\n" f"\nOur best guess font size is {font_size}\n"