550 lines
14 KiB
Python
550 lines
14 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/>.
|
|
|
|
"""
|
|
Position info and display
|
|
|
|
"""
|
|
from typing import Optional, Dict, Any, Callable
|
|
from functools import partial
|
|
from math import floor
|
|
|
|
from pyqtgraph import Point, functions as fn
|
|
from pydantic import BaseModel
|
|
from PyQt5 import QtGui, QtWidgets
|
|
from PyQt5.QtCore import QPointF
|
|
from PyQt5.QtGui import QGraphicsPathItem
|
|
|
|
from ._annotate import mk_marker_path
|
|
from ._anchors import (
|
|
marker_right_points,
|
|
gpath_pin,
|
|
# keep_marker_in_view,
|
|
)
|
|
from ._label import Label
|
|
from ._lines import LevelLine, level_line
|
|
from ._style import _font
|
|
from ..data._source import Symbol
|
|
|
|
|
|
class Position(BaseModel):
|
|
'''Basic pp representation with attached fills history.
|
|
|
|
'''
|
|
symbol: Symbol
|
|
size: float
|
|
avg_price: float # TODO: contextual pricing
|
|
fills: Dict[str, Any] = {}
|
|
|
|
|
|
class LevelMarker(QGraphicsPathItem):
|
|
'''An arrow marker path graphich which redraws itself
|
|
to the specified view coordinate level on each paint cycle.
|
|
|
|
'''
|
|
def __init__(
|
|
self,
|
|
chart: 'ChartPlotWidget', # noqa
|
|
style: str,
|
|
get_level: Callable[..., float],
|
|
size: float = 20,
|
|
keep_in_view: bool = True,
|
|
|
|
) -> None:
|
|
|
|
self._style = None
|
|
self.size = size
|
|
|
|
# get polygon and scale
|
|
super().__init__()
|
|
|
|
# interally generates and scales path
|
|
self.style = style
|
|
# path = mk_marker_path(style)
|
|
|
|
# self.scale(size, size)
|
|
|
|
self.chart = chart
|
|
# chart.getViewBox().scene().addItem(self)
|
|
|
|
self.get_level = get_level
|
|
self.scene_x = lambda: marker_right_points(chart)[1]
|
|
self.level: float = 0
|
|
self.keep_in_view = keep_in_view
|
|
|
|
# get the path for the opaque path **without** weird
|
|
# surrounding margin
|
|
self.path_br = self.mapToScene(
|
|
self.path()
|
|
).boundingRect()
|
|
|
|
|
|
@property
|
|
def style(self) -> str:
|
|
return self._style
|
|
|
|
@style.setter
|
|
def style(self, value: str) -> None:
|
|
if self._style != value:
|
|
polygon = mk_marker_path(value)
|
|
self.setPath(polygon)
|
|
self._style = value
|
|
self.scale(self.size, self.size)
|
|
|
|
def delete(self) -> None:
|
|
self.scene().removeItem(self)
|
|
|
|
@property
|
|
def h(self) -> float:
|
|
return self.path_br.height()
|
|
|
|
@property
|
|
def w(self) -> float:
|
|
return self.path_br.width()
|
|
|
|
def position_in_view(
|
|
self,
|
|
# level: float,
|
|
|
|
) -> None:
|
|
'''Show a pp off-screen indicator for a level label.
|
|
|
|
This is like in fps games where you have a gps "nav" indicator
|
|
but your teammate is outside the range of view, except in 2D, on
|
|
the y-dimension.
|
|
|
|
'''
|
|
level = self.get_level()
|
|
|
|
view = self.chart.getViewBox()
|
|
vr = view.state['viewRange']
|
|
ymn, ymx = vr[1]
|
|
|
|
# _, marker_right, _ = marker_right_points(line._chart)
|
|
x = self.scene_x()
|
|
|
|
if level > ymx: # pin to top of view
|
|
self.setPos(
|
|
QPointF(
|
|
x,
|
|
self.h/3,
|
|
)
|
|
)
|
|
|
|
elif level < ymn: # pin to bottom of view
|
|
|
|
self.setPos(
|
|
QPointF(
|
|
x,
|
|
view.height() - 4/3*self.h,
|
|
)
|
|
)
|
|
|
|
else:
|
|
# # pp line is viewable so show marker normally
|
|
# self.update()
|
|
self.setPos(
|
|
x,
|
|
self.chart.view.mapFromView(
|
|
QPointF(0, self.get_level())
|
|
).y()
|
|
)
|
|
|
|
# marker = line._marker
|
|
if getattr(self, 'label', None):
|
|
label = self.label
|
|
|
|
# re-anchor label (i.e. trigger call of ``arrow_tr()`` from above
|
|
label.update()
|
|
|
|
def paint(
|
|
self,
|
|
|
|
p: QtGui.QPainter,
|
|
opt: QtWidgets.QStyleOptionGraphicsItem,
|
|
w: QtWidgets.QWidget
|
|
|
|
) -> None:
|
|
'''Core paint which we override to always update
|
|
our marker position in scene coordinates from a
|
|
view cooridnate "level".
|
|
|
|
'''
|
|
if self.keep_in_view:
|
|
self.position_in_view()
|
|
|
|
else:
|
|
|
|
# just place at desired level even if not in view
|
|
self.setPos(
|
|
self.scene_x(),
|
|
self.mapToScene(QPointF(0, self.get_level())).y()
|
|
)
|
|
|
|
return super().paint(p, opt, w)
|
|
|
|
|
|
class PositionInfo:
|
|
|
|
# inputs
|
|
chart: 'ChartPlotWidget' # noqa
|
|
info: dict
|
|
|
|
# allocated
|
|
pp_label: Label
|
|
size_label: Label
|
|
info_label: Label
|
|
line: Optional[LevelLine] = None
|
|
|
|
_color: str = 'default_light'
|
|
|
|
def __init__(
|
|
self,
|
|
chart: 'ChartPlotWidget', # noqa
|
|
|
|
) -> None:
|
|
|
|
# from . import _lines
|
|
|
|
self.chart = chart
|
|
self.info = {}
|
|
self.pp_label = None
|
|
|
|
view = chart.getViewBox()
|
|
|
|
# create placeholder 'up' level arrow
|
|
self._level_marker = None
|
|
self._level_marker = self.level_marker(size=1)
|
|
|
|
# literally 'pp' label that's always in view
|
|
self.pp_label = pp_label = Label(
|
|
view=view,
|
|
fmt_str='pp',
|
|
color=self._color,
|
|
update_on_range_change=False,
|
|
)
|
|
|
|
self._level_marker.label = pp_label
|
|
|
|
pp_label.scene_anchor = partial(
|
|
gpath_pin,
|
|
gpath=self._level_marker,
|
|
label=pp_label,
|
|
)
|
|
pp_label.render()
|
|
pp_label.show()
|
|
|
|
self.size_label = size_label = Label(
|
|
|
|
view=view,
|
|
color=self._color,
|
|
|
|
# this is "static" label
|
|
# update_on_range_change=False,
|
|
fmt_str='\n'.join((
|
|
'{entry_size} x',
|
|
)),
|
|
|
|
fields={
|
|
'entry_size': 0,
|
|
},
|
|
)
|
|
size_label.render()
|
|
# size_label.scene_anchor = self.align_to_marker
|
|
size_label.scene_anchor = partial(
|
|
gpath_pin,
|
|
location_description='left-of-path-centered',
|
|
gpath=self._level_marker,
|
|
label=size_label,
|
|
)
|
|
size_label.hide()
|
|
|
|
# self.info_label = info_label = Label(
|
|
|
|
# view=view,
|
|
# color=self._color,
|
|
|
|
# # this is "static" label
|
|
# # update_on_range_change=False,
|
|
|
|
# fmt_str='\n'.join((
|
|
# # '{entry_size}x ',
|
|
# '{percent_pnl} % PnL',
|
|
# # '{percent_of_port}% of port',
|
|
# '${base_unit_value}',
|
|
# )),
|
|
|
|
# fields={
|
|
# # 'entry_size': 0,
|
|
# 'percent_pnl': 0,
|
|
# 'percent_of_port': 2,
|
|
# 'base_unit_value': '1k',
|
|
# },
|
|
# )
|
|
# info_label.scene_anchor = lambda: self.size_label.txt.pos()
|
|
# + QPointF(0, self.size_label.h)
|
|
# info_label.render()
|
|
# info_label.hide()
|
|
|
|
def level(self) -> float:
|
|
if self.line:
|
|
return self.line.value()
|
|
else:
|
|
return 0
|
|
|
|
def show(self) -> None:
|
|
self.pp_label.show()
|
|
self.size_label.show()
|
|
# self.info_label.show()
|
|
if self.line:
|
|
self.line.show()
|
|
|
|
def hide(self) -> None:
|
|
# self.pp_label.hide()
|
|
self.size_label.hide()
|
|
# self.info_label.hide()
|
|
|
|
# if self.line:
|
|
# self.line.hide()
|
|
|
|
def level_marker(
|
|
self,
|
|
size: float,
|
|
|
|
) -> QGraphicsPathItem:
|
|
|
|
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 = '|<'
|
|
direction = 'up'
|
|
|
|
elif size < 0:
|
|
style = '>|'
|
|
direction = 'down'
|
|
|
|
arrow = LevelMarker(
|
|
chart=self.chart,
|
|
style=style,
|
|
get_level=self.level,
|
|
size=arrow_size,
|
|
)
|
|
# _, marker_right, _ = marker_right_points(self.chart)
|
|
# arrow.scene_x = marker_right
|
|
|
|
# monkey-cache height for sizing on pp nav-hub
|
|
# arrow._height = path_br.height()
|
|
# arrow._width = path_br.width()
|
|
arrow._direction = direction
|
|
|
|
self.chart.getViewBox().scene().addItem(arrow)
|
|
arrow.show()
|
|
|
|
# arrow.label = self.pp_label
|
|
|
|
# inside ``LevelLine.pain()`` this is updates...
|
|
# we need a better way to have the label updated as frequenty
|
|
# as every paint call? Maybe use a better slot then the range
|
|
# change?
|
|
# self._level_marker.label = self.pp_label
|
|
|
|
return arrow
|
|
|
|
def position_line(
|
|
self,
|
|
|
|
size: float,
|
|
level: float,
|
|
|
|
orient_v: str = 'bottom',
|
|
|
|
) -> LevelLine:
|
|
'''Convenience routine to add a line graphic representing an order
|
|
execution submitted to the EMS via the chart's "order mode".
|
|
|
|
'''
|
|
self.line = line = level_line(
|
|
self.chart,
|
|
level,
|
|
color=self._color,
|
|
add_label=False,
|
|
hl_on_hover=False,
|
|
movable=False,
|
|
hide_xhair_on_hover=False,
|
|
use_marker_margin=True,
|
|
only_show_markers_on_hover=False,
|
|
always_show_labels=True,
|
|
)
|
|
|
|
if size > 0:
|
|
style = '|<'
|
|
elif size < 0:
|
|
style = '>|'
|
|
|
|
self._level_marker.style = style
|
|
|
|
# last_direction = self._level_marker._direction
|
|
# if (
|
|
# size < 0 and last_direction == 'up'
|
|
# ):
|
|
# self._level_marker = self.level_marker(size)
|
|
marker = self._level_marker
|
|
|
|
# add path to scene
|
|
# line.getViewBox().scene().addItem(marker)
|
|
|
|
# set marker color to same as line
|
|
marker.setPen(line.currentPen)
|
|
marker.setBrush(fn.mkBrush(line.currentPen.color()))
|
|
marker.level = level
|
|
marker.update()
|
|
marker.show()
|
|
|
|
# hide position marker when out of view (for now)
|
|
vb = line.getViewBox()
|
|
vb.sigRangeChanged.connect(marker.position_in_view)
|
|
|
|
line._labels.append(self.pp_label)
|
|
|
|
# XXX: uses new marker drawing approach
|
|
# line.add_marker(self._level_marker)
|
|
line.set_level(level)
|
|
|
|
# sanity check
|
|
line.update_labels({'level': level})
|
|
|
|
# vb.sigRangeChanged.connect(
|
|
# partial(keep_marker_in_view, chartview=vb, line=line)
|
|
# )
|
|
|
|
return line
|
|
|
|
# order line endpoint anchor
|
|
def align_to_marker(self) -> QPointF:
|
|
|
|
pp_line = self.line
|
|
if pp_line:
|
|
|
|
line_ep = pp_line.scene_endpoint()
|
|
# print(line_ep)
|
|
|
|
y_level_scene = line_ep.y()
|
|
# pp_y = pp_label.txt.pos().y()
|
|
|
|
# if y_level_scene > pp_y:
|
|
# y_level_scene = pp_y
|
|
|
|
# elif y_level_scene
|
|
mkr_pos = self._level_marker.pos()
|
|
|
|
left_of_mkr = QPointF(
|
|
# line_ep.x() - self.size_label.w,
|
|
mkr_pos.x() - self.size_label.w,
|
|
mkr_pos.y(),
|
|
# self._level_marker
|
|
# max(0, y_level_scene),
|
|
# min(
|
|
# pp_label.txt.pos().y()
|
|
# ),
|
|
)
|
|
return left_of_mkr
|
|
|
|
# return QPointF(
|
|
|
|
# marker_right_points(chart)[2] - pp_label.w ,
|
|
# view.height() - pp_label.h,
|
|
# # br.x() - pp_label.w,
|
|
# # br.y(),
|
|
# )
|
|
|
|
else:
|
|
# pp = _lines._pp_label.txt
|
|
# scene_rect = pp.mapToScene(pp.boundingRect()).boundingRect()
|
|
# br = scene_rect.bottomRight()
|
|
|
|
return QPointF(0, 0)
|
|
|
|
def update_line(
|
|
self,
|
|
|
|
price: float,
|
|
size: float,
|
|
|
|
) -> None:
|
|
'''Update personal position level line.
|
|
|
|
|
|
'''
|
|
# do line update
|
|
line = self.line
|
|
|
|
if line is None and size:
|
|
|
|
# create and show a pp line
|
|
line = self.line = self.position_line(
|
|
level=price,
|
|
size=size,
|
|
)
|
|
line.show()
|
|
|
|
elif line:
|
|
|
|
if size != 0.0:
|
|
line.set_level(price)
|
|
self._level_marker.lelvel = price
|
|
self._level_marker.update()
|
|
line.update_labels({'size': size})
|
|
line.show()
|
|
|
|
else:
|
|
# remove pp line from view
|
|
line.delete()
|
|
self.line = None
|
|
|
|
def update(
|
|
self,
|
|
|
|
avg_price: float,
|
|
size: float,
|
|
|
|
) -> None:
|
|
'''Update graphics and data from average price and size.
|
|
|
|
'''
|
|
self.update_line(avg_price, size)
|
|
|
|
self._level_marker.level = avg_price
|
|
self._level_marker.update() # trigger paint
|
|
|
|
# info updates
|
|
self.info['avg_price'] = avg_price
|
|
self.info['size'] = size
|
|
|
|
# label updates
|
|
self.size_label.fields['entry_size'] = size
|
|
self.size_label.render()
|
|
|
|
# self.info_label.fields['size'] = size
|
|
# self.info_label.render()
|