# 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 . """ Double auction top-of-book (L1) graphics. """ from typing import Tuple import pyqtgraph as pg from piker.ui.qt import ( QPointF, QtCore, QtGui, ) 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_br_offset: float = -16 _y_txt_h_scaling: float = 2 # adjustment "further away from" anchor point _x_offset = 0 _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 = 'default_light', orient_v: str = 'bottom', orient_h: str = 'right', opacity: float = 1, # 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) self.setZValue(10) @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), width=3, ) 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() elif self._orient_v == 'top': lp, rp = rect.bottomLeft(), rect.bottomRight() p.drawLine( *map(int, [ lp.x(), lp.y(), rp.x(), rp.y(), ]) ) p.fillRect( self.rect, self.bg_color, ) 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': .9, 'font_size': font_size, 'fg_color': 'default_light', 'bg_color': chart.view_color, # normally 'papas_special' } # TODO: add humanized source-asset # info format. fmt_str = ( ' {size:.{size_digits}f} u' # '{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='\n' + 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()