Move L1 labels into lone module
parent
8997b6029b
commit
a1a1dec885
|
@ -41,8 +41,8 @@ from ._graphics._cursor import (
|
|||
from ._graphics._lines import (
|
||||
level_line,
|
||||
order_line,
|
||||
L1Labels,
|
||||
)
|
||||
from ._l1 import L1Labels
|
||||
from ._graphics._ohlc import BarItems
|
||||
from ._graphics._curve import FastAppendCurve
|
||||
from ._style import (
|
||||
|
@ -1105,7 +1105,7 @@ async def _async_main(
|
|||
):
|
||||
|
||||
# show line label once order is live
|
||||
line = order_mode.on_submit(oid)
|
||||
order_mode.on_submit(oid)
|
||||
|
||||
# resp to 'cancel' request or error condition
|
||||
# for action request
|
||||
|
@ -1132,7 +1132,7 @@ async def _async_main(
|
|||
price=msg['trigger_price'],
|
||||
arrow_index=get_index(time.time())
|
||||
)
|
||||
line = await order_mode.on_exec(oid, msg)
|
||||
await order_mode.on_exec(oid, msg)
|
||||
|
||||
# response to completed 'action' request for buy/sell
|
||||
elif resp in (
|
||||
|
|
|
@ -22,274 +22,12 @@ from typing import Tuple, Optional, List
|
|||
|
||||
import pyqtgraph as pg
|
||||
from PyQt5 import QtCore, QtGui
|
||||
from PyQt5.QtCore import QPointF
|
||||
|
||||
from .._label import Label, vbr_left, right_axis
|
||||
from .._style import (
|
||||
hcolor,
|
||||
_down_2_font_inches_we_like,
|
||||
)
|
||||
from .._axes import YAxisLabel
|
||||
|
||||
|
||||
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._chart.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 = _max_l1_line_len
|
||||
|
||||
self.setPos(QPointF(
|
||||
self._h_shift * (w + self._x_offset),
|
||||
abs_pos.y() + self._v_shift * h
|
||||
))
|
||||
|
||||
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(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()
|
||||
|
||||
|
||||
# global for now but probably should be
|
||||
# attached to chart instance?
|
||||
_max_l1_line_len: float = 0
|
||||
|
||||
|
||||
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.
|
||||
global _max_l1_line_len
|
||||
_max_l1_line_len = max(_max_l1_line_len, w)
|
||||
|
||||
return h, w
|
||||
|
||||
|
||||
class L1Labels:
|
||||
"""Level 1 bid ask labels for dynamic update on price-axis.
|
||||
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
chart: 'ChartPlotWidget', # noqa
|
||||
digits: int = 2,
|
||||
size_digits: int = 3,
|
||||
font_size_inches: float = _down_2_font_inches_we_like,
|
||||
) -> None:
|
||||
|
||||
self.chart = chart
|
||||
|
||||
raxis = chart.getAxis('right')
|
||||
kwargs = {
|
||||
'chart': chart,
|
||||
'parent': raxis,
|
||||
|
||||
'opacity': 1,
|
||||
'font_size_inches': font_size_inches,
|
||||
'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()
|
||||
|
||||
|
||||
# TODO: probably worth investigating if we can
|
||||
|
@ -391,7 +129,7 @@ class LevelLine(pg.InfiniteLine):
|
|||
bg_color: str = None,
|
||||
|
||||
**label_kwargs,
|
||||
) -> LevelLabel:
|
||||
) -> Label:
|
||||
"""Add a ``LevelLabel`` anchored at one of the line endpoints in view.
|
||||
|
||||
"""
|
||||
|
|
|
@ -0,0 +1,291 @@
|
|||
# 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,
|
||||
_down_2_font_inches_we_like,
|
||||
)
|
||||
|
||||
|
||||
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._chart.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 = _max_l1_line_len
|
||||
|
||||
self.setPos(QPointF(
|
||||
self._h_shift * (w + self._x_offset),
|
||||
abs_pos.y() + self._v_shift * h
|
||||
))
|
||||
|
||||
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(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()
|
||||
|
||||
|
||||
# global for now but probably should be
|
||||
# attached to chart instance?
|
||||
_max_l1_line_len: float = 0
|
||||
|
||||
|
||||
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.
|
||||
global _max_l1_line_len
|
||||
_max_l1_line_len = max(_max_l1_line_len, w)
|
||||
|
||||
return h, w
|
||||
|
||||
|
||||
class L1Labels:
|
||||
"""Level 1 bid ask labels for dynamic update on price-axis.
|
||||
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
chart: 'ChartPlotWidget', # noqa
|
||||
digits: int = 2,
|
||||
size_digits: int = 3,
|
||||
font_size_inches: float = _down_2_font_inches_we_like,
|
||||
) -> None:
|
||||
|
||||
self.chart = chart
|
||||
|
||||
raxis = chart.getAxis('right')
|
||||
kwargs = {
|
||||
'chart': chart,
|
||||
'parent': raxis,
|
||||
|
||||
'opacity': 1,
|
||||
'font_size_inches': font_size_inches,
|
||||
'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()
|
Loading…
Reference in New Issue