piker/piker/ui/_l1.py

298 lines
7.2 KiB
Python

# piker: trading gear for hackers
# Copyright (C) Tyler Goodlet (in stewardship for piker0)
# 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
Double auction top-of-book (L1) graphics.
"""
from typing import Tuple
import pyqtgraph as pg
from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import QPointF
from ._axes import YAxisLabel
from ._style import hcolor
from ._pg_overrides import PlotItem
class LevelLabel(YAxisLabel):
"""Y-axis (vertically) oriented, horizontal label that sticks to
where it's placed despite chart resizing and supports displaying
multiple fields.
TODO: replace the rectangle-text part with our new ``Label`` type.
"""
_x_margin = 0
_y_margin = 0
# adjustment "further away from" anchor point
_x_offset = 9
_y_offset = 0
# fields to be displayed in the label string
_fields = {
'level': 0,
'level_digits': 2,
}
# default label template is just a y-level with so much precision
_fmt_str = '{level:,.{level_digits}f} '
def __init__(
self,
chart,
parent,
color: str = 'bracket',
orient_v: str = 'bottom',
orient_h: str = 'left',
opacity: float = 0,
# makes order line labels offset from their parent axis
# such that they don't collide with the L1/L2 lines/prices
# that are displayed on the axis
adjust_to_l1: bool = False,
**axis_label_kwargs,
) -> None:
super().__init__(
chart,
parent=parent,
use_arrow=False,
opacity=opacity,
**axis_label_kwargs
)
# TODO: this is kinda cludgy
self._hcolor: pg.Pen = None
self.color: str = color
# orientation around axis options
self._orient_v = orient_v
self._orient_h = orient_h
self._adjust_to_l1 = adjust_to_l1
self._v_shift = {
'top': -1.,
'bottom': 0.,
'middle': 1 / 2.
}[orient_v]
self._h_shift = {
'left': -1.,
'right': 0.
}[orient_h]
self.fields = self._fields.copy()
# ensure default format fields are in correct
self.set_fmt_str(self._fmt_str, self.fields)
@property
def color(self):
return self._hcolor
@color.setter
def color(self, color: str) -> None:
self._hcolor = color
self._pen = self.pen = pg.mkPen(hcolor(color))
def update_on_resize(self, vr, r):
"""Tiis is a ``.sigRangeChanged()`` handler.
"""
self.update_fields(self.fields)
def update_fields(
self,
fields: dict = None,
) -> None:
"""Update the label's text contents **and** position from
a view box coordinate datum.
"""
self.fields.update(fields)
level = self.fields['level']
# map "level" to local coords
abs_xy = self._pi.mapFromView(QPointF(0, level))
self.update_label(
abs_xy,
self.fields,
)
def update_label(
self,
abs_pos: QPointF, # scene coords
fields: dict,
) -> None:
# write contents, type specific
h, w = self.set_label_str(fields)
if self._adjust_to_l1:
self._x_offset = self._pi.chart_widget._max_l1_line_len
self.setPos(QPointF(
self._h_shift * (w + self._x_offset),
abs_pos.y() + self._v_shift * h
))
# XXX: definitely need this!
self.update()
def set_fmt_str(
self,
fmt_str: str,
fields: dict,
) -> (str, str):
# test that new fmt str can be rendered
self._fmt_str = fmt_str
self.set_label_str(fields)
self.fields.update(fields)
return fmt_str, self.label_str
def set_label_str(
self,
fields: dict,
):
# use space as e3 delim
self.label_str = self._fmt_str.format(**fields).replace(',', ' ')
br = self.boundingRect()
h, w = br.height(), br.width()
return h, w
def size_hint(self) -> Tuple[None, None]:
return None, None
def draw(
self,
p: QtGui.QPainter,
rect: QtCore.QRectF
) -> None:
p.setPen(self._pen)
rect = self.rect
if self._orient_v == 'bottom':
lp, rp = rect.topLeft(), rect.topRight()
# p.drawLine(rect.topLeft(), rect.topRight())
elif self._orient_v == 'top':
lp, rp = rect.bottomLeft(), rect.bottomRight()
p.drawLine(
*map(int, [
lp.x(),
lp.y(),
rp.x(),
rp.y(),
])
)
def highlight(self, pen) -> None:
self._pen = pen
self.update()
def unhighlight(self):
self._pen = self.pen
self.update()
class L1Label(LevelLabel):
text_flags = (
QtCore.Qt.TextDontClip
| QtCore.Qt.AlignLeft
)
def set_label_str(
self,
fields: dict,
) -> None:
"""Make sure the max L1 line module var is kept up to date.
"""
h, w = super().set_label_str(fields)
# Set a global "max L1 label length" so we can
# look it up on order lines and adjust their
# labels not to overlap with it.
chart = self._pi.chart_widget
chart._max_l1_line_len: float = max(
chart._max_l1_line_len,
w,
)
return h, w
class L1Labels:
"""Level 1 bid ask labels for dynamic update on price-axis.
"""
def __init__(
self,
plotitem: PlotItem,
digits: int = 2,
size_digits: int = 3,
font_size: str = 'small',
) -> None:
chart = self.chart = plotitem.chart_widget
raxis = plotitem.getAxis('right')
kwargs = {
'chart': plotitem,
'parent': raxis,
'opacity': 1,
'font_size': font_size,
'fg_color': chart.pen_color,
'bg_color': chart.view_color,
}
fmt_str = (
' {size:.{size_digits}f} x '
'{level:,.{level_digits}f} '
)
fields = {
'level': 0,
'level_digits': digits,
'size': 0,
'size_digits': size_digits,
}
bid = self.bid_label = L1Label(
orient_v='bottom',
**kwargs,
)
bid.set_fmt_str(fmt_str=fmt_str, fields=fields)
bid.show()
ask = self.ask_label = L1Label(
orient_v='top',
**kwargs,
)
ask.set_fmt_str(fmt_str=fmt_str, fields=fields)
ask.show()