Compare commits
10 Commits
649798a91f
...
bb9f6475f4
Author | SHA1 | Date |
---|---|---|
Tyler Goodlet | bb9f6475f4 | |
Tyler Goodlet | 54463b3595 | |
Tyler Goodlet | aaf234cbaf | |
Tyler Goodlet | 0f6589d9ff | |
Tyler Goodlet | 30d8e096c6 | |
Tyler Goodlet | 19609178ce | |
Tyler Goodlet | 4c39407363 | |
Tyler Goodlet | a345daa522 | |
Tyler Goodlet | ea75281cbc | |
Tyler Goodlet | 86a1f33abb |
|
@ -1,5 +1,5 @@
|
||||||
# piker: trading gear for hackers.
|
# piker: trading gear for hackers.
|
||||||
# Copyright 2018 Tyler Goodlet
|
# Copyright 2018-forever Tyler Goodlet and p0
|
||||||
|
|
||||||
# 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
|
||||||
|
@ -16,6 +16,23 @@
|
||||||
|
|
||||||
"""
|
"""
|
||||||
piker: trading gear for hackers.
|
piker: trading gear for hackers.
|
||||||
|
|
||||||
|
If your boss told you to checkout this code base, stop, right the fuck now.
|
||||||
|
Tell your boss this code base is absolute shit and you'll never be able to
|
||||||
|
make it "work" due to scaling and that Python is too slow. If you even for
|
||||||
|
a second suggest to them that it's useful you'll be hearing from us.
|
||||||
|
|
||||||
|
If you're a yabf (yet another backtesting framework) author/maintainer, also stop.
|
||||||
|
We don't slurp callback soup; your code can be called from any FSP routine and
|
||||||
|
``piker`` requires no adoption of your "framework". If you want to provide an
|
||||||
|
integration, cool, but we aren't going to adopt any oudated concurrency models.
|
||||||
|
|
||||||
|
This code is for **you** as a person to defend yourself from our messed
|
||||||
|
up finacial system and the charlatans and phonies who run it.
|
||||||
|
|
||||||
|
``piker`` will never be for sale nor influenced by suits of any type.
|
||||||
|
|
||||||
|
Join us, let's make the best software linux has ever seen.
|
||||||
"""
|
"""
|
||||||
import msgpack # noqa
|
import msgpack # noqa
|
||||||
import msgpack_numpy
|
import msgpack_numpy
|
||||||
|
|
|
@ -142,7 +142,7 @@ class Client:
|
||||||
# durationStr='1 D',
|
# durationStr='1 D',
|
||||||
|
|
||||||
# time length calcs
|
# time length calcs
|
||||||
durationStr='{count} S'.format(count=2000 * 5),
|
durationStr='{count} S'.format(count=1000 * 5),
|
||||||
barSizeSetting='5 secs',
|
barSizeSetting='5 secs',
|
||||||
|
|
||||||
# always use extended hours
|
# always use extended hours
|
||||||
|
@ -225,18 +225,22 @@ class Client:
|
||||||
# use heuristics to figure out contract "type"
|
# use heuristics to figure out contract "type"
|
||||||
sym, exch = symbol.upper().split('.')
|
sym, exch = symbol.upper().split('.')
|
||||||
|
|
||||||
# TODO: metadata system for all these exchange rules..
|
# futes
|
||||||
if exch in ('PURE',):
|
|
||||||
currency = 'CAD'
|
|
||||||
|
|
||||||
if exch in ('GLOBEX', 'NYMEX', 'CME', 'CMECRYPTO'):
|
if exch in ('GLOBEX', 'NYMEX', 'CME', 'CMECRYPTO'):
|
||||||
con = await self.get_cont_fute(symbol=sym, exchange=exch)
|
con = await self.get_cont_fute(symbol=sym, exchange=exch)
|
||||||
|
|
||||||
|
# commodities
|
||||||
elif exch == 'CMDTY': # eg. XAUUSD.CMDTY
|
elif exch == 'CMDTY': # eg. XAUUSD.CMDTY
|
||||||
con_kwargs, bars_kwargs = _adhoc_cmdty_data_map[sym]
|
con_kwargs, bars_kwargs = _adhoc_cmdty_data_map[sym]
|
||||||
con = ibis.Commodity(**con_kwargs)
|
con = ibis.Commodity(**con_kwargs)
|
||||||
con.bars_kwargs = bars_kwargs
|
con.bars_kwargs = bars_kwargs
|
||||||
|
|
||||||
|
# stonks
|
||||||
else:
|
else:
|
||||||
|
# TODO: metadata system for all these exchange rules..
|
||||||
|
if exch in ('PURE', 'TSE'): # non-yankee
|
||||||
|
currency = 'CAD'
|
||||||
|
|
||||||
con = ibis.Stock(symbol=sym, exchange=exch, currency=currency)
|
con = ibis.Stock(symbol=sym, exchange=exch, currency=currency)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -454,6 +458,8 @@ def normalize(
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: figure out how to share quote feeds sanely despite
|
||||||
|
# the wacky ``ib_insync`` api.
|
||||||
# @tractor.msg.pub
|
# @tractor.msg.pub
|
||||||
async def stream_quotes(
|
async def stream_quotes(
|
||||||
symbols: List[str],
|
symbols: List[str],
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
"""
|
"""
|
||||||
Chart axes graphics and behavior.
|
Chart axes graphics and behavior.
|
||||||
"""
|
"""
|
||||||
|
import time
|
||||||
|
from functools import partial
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
# import numpy as np
|
||||||
|
import pandas as pd
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from PyQt5 import QtCore, QtGui
|
from PyQt5 import QtCore, QtGui
|
||||||
from PyQt5.QtCore import QPointF
|
from PyQt5.QtCore import QPointF
|
||||||
|
|
||||||
|
|
||||||
# from .quantdom.base import Quotes
|
|
||||||
from .quantdom.utils import fromtimestamp
|
from .quantdom.utils import fromtimestamp
|
||||||
from ._style import _font, hcolor
|
from ._style import _font, hcolor
|
||||||
|
|
||||||
|
@ -15,7 +20,6 @@ class PriceAxis(pg.AxisItem):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
# chart: 'ChartPlotWidget',
|
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(orientation='right')
|
super().__init__(orientation='right')
|
||||||
self.setStyle(**{
|
self.setStyle(**{
|
||||||
|
@ -28,10 +32,7 @@ class PriceAxis(pg.AxisItem):
|
||||||
})
|
})
|
||||||
self.setLabel(**{'font-size': '10pt'})
|
self.setLabel(**{'font-size': '10pt'})
|
||||||
self.setTickFont(_font)
|
self.setTickFont(_font)
|
||||||
self.setWidth(150)
|
self.setWidth(125)
|
||||||
# self.chart = chart
|
|
||||||
# accesed normally via
|
|
||||||
# .getAxis('right')
|
|
||||||
|
|
||||||
# XXX: drop for now since it just eats up h space
|
# XXX: drop for now since it just eats up h space
|
||||||
|
|
||||||
|
@ -43,9 +44,20 @@ class PriceAxis(pg.AxisItem):
|
||||||
|
|
||||||
|
|
||||||
class DynamicDateAxis(pg.AxisItem):
|
class DynamicDateAxis(pg.AxisItem):
|
||||||
tick_tpl = {'D1': '%Y-%b-%d'}
|
# time formats mapped by seconds between bars
|
||||||
|
tick_tpl = {
|
||||||
|
60*60*24: '%Y-%b-%d',
|
||||||
|
60: '%H:%M',
|
||||||
|
30: '%H:%M:%S',
|
||||||
|
5: '%H:%M:%S',
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, linked_charts, *args, **kwargs):
|
def __init__(
|
||||||
|
self,
|
||||||
|
linked_charts,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
) -> None:
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.linked_charts = linked_charts
|
self.linked_charts = linked_charts
|
||||||
self.setTickFont(_font)
|
self.setTickFont(_font)
|
||||||
|
@ -59,24 +71,25 @@ class DynamicDateAxis(pg.AxisItem):
|
||||||
)
|
)
|
||||||
# self.setHeight(35)
|
# self.setHeight(35)
|
||||||
|
|
||||||
def tickStrings(self, values, scale, spacing):
|
def _indexes_to_timestrs(
|
||||||
# if len(values) > 1 or not values:
|
self,
|
||||||
# values = Quotes.time
|
indexes: List[int],
|
||||||
|
) -> List[str]:
|
||||||
# strings = super().tickStrings(values, scale, spacing)
|
|
||||||
s_period = 'D1'
|
|
||||||
strings = []
|
|
||||||
bars = self.linked_charts.chart._array
|
bars = self.linked_charts.chart._array
|
||||||
quotes_count = len(bars) - 1
|
times = bars['time']
|
||||||
|
bars_len = len(bars)
|
||||||
|
delay = times[-1] - times[times != times[-1]][-1]
|
||||||
|
|
||||||
for ibar in values:
|
epochs = times[list(
|
||||||
if ibar > quotes_count:
|
map(int, filter(lambda i: i < bars_len, indexes))
|
||||||
return strings
|
)]
|
||||||
dt_tick = fromtimestamp(bars[int(ibar)]['time'])
|
# TODO: **don't** have this hard coded shift to EST
|
||||||
strings.append(
|
dts = pd.to_datetime(epochs, unit='s') - 4*pd.offsets.Hour()
|
||||||
dt_tick.strftime(self.tick_tpl[s_period])
|
return dts.strftime(self.tick_tpl[delay])
|
||||||
)
|
|
||||||
return strings
|
|
||||||
|
def tickStrings(self, values: List[float], scale, spacing):
|
||||||
|
return self._indexes_to_timestrs(values)
|
||||||
|
|
||||||
|
|
||||||
class AxisLabel(pg.GraphicsObject):
|
class AxisLabel(pg.GraphicsObject):
|
||||||
|
@ -88,7 +101,7 @@ class AxisLabel(pg.GraphicsObject):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
parent=None,
|
parent=None,
|
||||||
digits=1,
|
digits=2,
|
||||||
color=None,
|
color=None,
|
||||||
opacity=1,
|
opacity=1,
|
||||||
**kwargs
|
**kwargs
|
||||||
|
@ -128,6 +141,7 @@ class AxisLabel(pg.GraphicsObject):
|
||||||
p.drawText(option.rect, self.text_flags, self.label_str)
|
p.drawText(option.rect, self.text_flags, self.label_str)
|
||||||
|
|
||||||
# uggggghhhh
|
# uggggghhhh
|
||||||
|
|
||||||
def tick_to_string(self, tick_pos):
|
def tick_to_string(self, tick_pos):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@ -160,24 +174,20 @@ class XAxisLabel(AxisLabel):
|
||||||
)
|
)
|
||||||
# text_flags = _common_text_flags
|
# text_flags = _common_text_flags
|
||||||
|
|
||||||
def tick_to_string(self, tick_pos):
|
|
||||||
# TODO: change to actual period
|
|
||||||
tpl = self.parent.tick_tpl['D1']
|
|
||||||
bars = self.parent.linked_charts.chart._array
|
|
||||||
if tick_pos > len(bars):
|
|
||||||
return 'Unknown Time'
|
|
||||||
return fromtimestamp(bars[round(tick_pos)]['time']).strftime(tpl)
|
|
||||||
|
|
||||||
def boundingRect(self): # noqa
|
def boundingRect(self): # noqa
|
||||||
return QtCore.QRectF(0, 0, 145, 40)
|
# TODO: we need to get the parent axe's dimensions transformed
|
||||||
|
# to abs coords to be 100% correct here:
|
||||||
|
# self.parent.boundingRect()
|
||||||
|
return QtCore.QRectF(0, 0, 100, 31)
|
||||||
|
|
||||||
def update_label(self, abs_pos, data):
|
def update_label(
|
||||||
# ibar = view_pos.x()
|
self,
|
||||||
# if ibar > self.quotes_count:
|
abs_pos: QPointF, # scene coords
|
||||||
# return
|
data: float, # data for text
|
||||||
self.label_str = self.tick_to_string(data)
|
offset: int = 0 # if have margins, k?
|
||||||
|
) -> None:
|
||||||
|
self.label_str = self.parent._indexes_to_timestrs([int(data)])[0]
|
||||||
width = self.boundingRect().width()
|
width = self.boundingRect().width()
|
||||||
offset = 0 # if have margins
|
|
||||||
new_pos = QPointF(abs_pos.x() - width / 2 - offset, 0)
|
new_pos = QPointF(abs_pos.x() - width / 2 - offset, 0)
|
||||||
self.setPos(new_pos)
|
self.setPos(new_pos)
|
||||||
|
|
||||||
|
@ -202,10 +212,10 @@ class YAxisLabel(AxisLabel):
|
||||||
self,
|
self,
|
||||||
abs_pos: QPointF, # scene coords
|
abs_pos: QPointF, # scene coords
|
||||||
data: float, # data for text
|
data: float, # data for text
|
||||||
|
offset: int = 0 # if have margins, k?
|
||||||
) -> None:
|
) -> None:
|
||||||
self.label_str = self.tick_to_string(data)
|
self.label_str = self.tick_to_string(data)
|
||||||
height = self.boundingRect().height()
|
height = self.boundingRect().height()
|
||||||
offset = 0 # if have margins
|
|
||||||
new_pos = QPointF(0, abs_pos.y() - height / 2 - offset)
|
new_pos = QPointF(0, abs_pos.y() - height / 2 - offset)
|
||||||
self.setPos(new_pos)
|
self.setPos(new_pos)
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,8 @@ from ._axes import (
|
||||||
PriceAxis,
|
PriceAxis,
|
||||||
)
|
)
|
||||||
from ._graphics import CrossHair, BarItems
|
from ._graphics import CrossHair, BarItems
|
||||||
from ._style import _xaxis_at, _min_points_to_show
|
from ._axes import YSticky
|
||||||
|
from ._style import _xaxis_at, _min_points_to_show, hcolor
|
||||||
from ._source import Symbol
|
from ._source import Symbol
|
||||||
from .. import brokers
|
from .. import brokers
|
||||||
from .. import data
|
from .. import data
|
||||||
|
@ -29,7 +30,7 @@ from .. import fsp
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
# margins
|
# margins
|
||||||
CHART_MARGINS = (0, 0, 10, 3)
|
CHART_MARGINS = (0, 0, 5, 3)
|
||||||
|
|
||||||
|
|
||||||
class ChartSpace(QtGui.QWidget):
|
class ChartSpace(QtGui.QWidget):
|
||||||
|
@ -42,7 +43,7 @@ class ChartSpace(QtGui.QWidget):
|
||||||
self.v_layout = QtGui.QVBoxLayout(self)
|
self.v_layout = QtGui.QVBoxLayout(self)
|
||||||
self.v_layout.setContentsMargins(0, 0, 0, 0)
|
self.v_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
self.toolbar_layout = QtGui.QHBoxLayout()
|
self.toolbar_layout = QtGui.QHBoxLayout()
|
||||||
self.toolbar_layout.setContentsMargins(10, 10, 15, 0)
|
self.toolbar_layout.setContentsMargins(5, 5, 10, 0)
|
||||||
self.h_layout = QtGui.QHBoxLayout()
|
self.h_layout = QtGui.QHBoxLayout()
|
||||||
|
|
||||||
# self.init_timeframes_ui()
|
# self.init_timeframes_ui()
|
||||||
|
@ -225,7 +226,6 @@ class LinkedSplitCharts(QtGui.QWidget):
|
||||||
|
|
||||||
cpw.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
|
cpw.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
|
||||||
cpw.getPlotItem().setContentsMargins(*CHART_MARGINS)
|
cpw.getPlotItem().setContentsMargins(*CHART_MARGINS)
|
||||||
# self.splitter.addWidget(cpw)
|
|
||||||
|
|
||||||
# link chart x-axis to main quotes chart
|
# link chart x-axis to main quotes chart
|
||||||
cpw.setXLink(self.chart)
|
cpw.setXLink(self.chart)
|
||||||
|
@ -246,6 +246,9 @@ class LinkedSplitCharts(QtGui.QWidget):
|
||||||
# scale split regions
|
# scale split regions
|
||||||
self.set_split_sizes()
|
self.set_split_sizes()
|
||||||
|
|
||||||
|
# XXX: we need this right?
|
||||||
|
# self.splitter.addWidget(cpw)
|
||||||
|
|
||||||
return cpw
|
return cpw
|
||||||
|
|
||||||
|
|
||||||
|
@ -272,16 +275,19 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# the data view we generate graphics from
|
# the data view we generate graphics from
|
||||||
array: np.ndarray,
|
array: np.ndarray,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
# parent=None,
|
|
||||||
# background='default',
|
|
||||||
# plotItem=None,
|
|
||||||
):
|
):
|
||||||
"""Configure chart display settings.
|
"""Configure chart display settings.
|
||||||
"""
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(
|
||||||
|
background=hcolor('papas_special'),
|
||||||
|
# parent=None,
|
||||||
|
# plotItem=None,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
self._array = array # readonly view of data
|
self._array = array # readonly view of data
|
||||||
self._graphics = {} # registry of underlying graphics
|
self._graphics = {} # registry of underlying graphics
|
||||||
self._labels = {} # registry of underlying graphics
|
self._labels = {} # registry of underlying graphics
|
||||||
|
self._ysticks = {} # registry of underlying graphics
|
||||||
|
|
||||||
# show only right side axes
|
# show only right side axes
|
||||||
self.hideAxis('left')
|
self.hideAxis('left')
|
||||||
|
@ -350,6 +356,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
self._graphics[name] = graphics
|
self._graphics[name] = graphics
|
||||||
|
|
||||||
# XXX: How to stack labels vertically?
|
# XXX: How to stack labels vertically?
|
||||||
|
# Ogi says: "
|
||||||
label = pg.LabelItem(
|
label = pg.LabelItem(
|
||||||
justify='left',
|
justify='left',
|
||||||
size='5pt',
|
size='5pt',
|
||||||
|
@ -373,6 +380,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# show last 50 points on startup
|
# show last 50 points on startup
|
||||||
self.plotItem.vb.setXRange(xlast - 50, xlast + 50)
|
self.plotItem.vb.setXRange(xlast - 50, xlast + 50)
|
||||||
|
|
||||||
|
self._add_sticky(name)
|
||||||
|
|
||||||
return graphics
|
return graphics
|
||||||
|
|
||||||
def draw_curve(
|
def draw_curve(
|
||||||
|
@ -421,6 +430,21 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
return curve
|
return curve
|
||||||
|
|
||||||
|
def _add_sticky(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
# retreive: Callable[None, np.ndarray],
|
||||||
|
) -> YSticky:
|
||||||
|
# add y-axis "last" value label
|
||||||
|
last = self._ysticks['last'] = YSticky(
|
||||||
|
chart=self,
|
||||||
|
parent=self.getAxis('right'),
|
||||||
|
# digits=0,
|
||||||
|
opacity=1,
|
||||||
|
color=pg.mkPen(hcolor('gray'))
|
||||||
|
)
|
||||||
|
return last
|
||||||
|
|
||||||
def update_from_array(
|
def update_from_array(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
|
@ -636,6 +660,10 @@ async def _async_main(
|
||||||
loglevel,
|
loglevel,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# update last price sticky
|
||||||
|
last = chart._ysticks['last']
|
||||||
|
last.update_from_data(*chart._array[-1][['index', 'close']])
|
||||||
|
|
||||||
# graphics update loop
|
# graphics update loop
|
||||||
|
|
||||||
async with data.open_feed(
|
async with data.open_feed(
|
||||||
|
@ -646,7 +674,7 @@ async def _async_main(
|
||||||
|
|
||||||
# wait for a first quote before we start any update tasks
|
# wait for a first quote before we start any update tasks
|
||||||
quote = await stream.__anext__()
|
quote = await stream.__anext__()
|
||||||
print(f'RECEIVED FIRST QUOTE {quote}')
|
log.info(f'RECEIVED FIRST QUOTE {quote}')
|
||||||
|
|
||||||
# start graphics tasks after receiving first live quote
|
# start graphics tasks after receiving first live quote
|
||||||
n.start_soon(add_new_bars, delay, linked_charts)
|
n.start_soon(add_new_bars, delay, linked_charts)
|
||||||
|
@ -675,6 +703,10 @@ async def _async_main(
|
||||||
chart.name,
|
chart.name,
|
||||||
chart._array,
|
chart._array,
|
||||||
)
|
)
|
||||||
|
# update sticky(s)
|
||||||
|
last = chart._ysticks['last']
|
||||||
|
last.update_from_data(
|
||||||
|
*chart._array[-1][['index', 'close']])
|
||||||
chart._set_yrange()
|
chart._set_yrange()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ from PyQt5 import QtCore, QtGui
|
||||||
from PyQt5.QtCore import QLineF
|
from PyQt5.QtCore import QLineF
|
||||||
|
|
||||||
from .quantdom.utils import timeit
|
from .quantdom.utils import timeit
|
||||||
from ._style import _xaxis_at # , _tina_mode
|
from ._style import _xaxis_at, hcolor
|
||||||
from ._axes import YAxisLabel, XAxisLabel
|
from ._axes import YAxisLabel, XAxisLabel
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,17 +18,28 @@ from ._axes import YAxisLabel, XAxisLabel
|
||||||
# - checkout pyqtgraph.PlotCurveItem.setCompositionMode
|
# - checkout pyqtgraph.PlotCurveItem.setCompositionMode
|
||||||
|
|
||||||
_mouse_rate_limit = 30
|
_mouse_rate_limit = 30
|
||||||
|
_debounce_delay = 10
|
||||||
|
_ch_label_opac = 1
|
||||||
|
|
||||||
|
|
||||||
class CrossHair(pg.GraphicsObject):
|
class CrossHair(pg.GraphicsObject):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
linkedsplitcharts: 'LinkedSplitCharts',
|
linkedsplitcharts: 'LinkedSplitCharts', # noqa
|
||||||
digits: int = 0
|
digits: int = 0
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.pen = pg.mkPen('#a9a9a9') # gray?
|
# XXX: not sure why these are instance variables?
|
||||||
|
# It's not like we can change them on the fly..?
|
||||||
|
self.pen = pg.mkPen(
|
||||||
|
color=hcolor('default'),
|
||||||
|
style=QtCore.Qt.DashLine,
|
||||||
|
)
|
||||||
|
self.lines_pen = pg.mkPen(
|
||||||
|
color='#a9a9a9', # gray?
|
||||||
|
style=QtCore.Qt.DashLine,
|
||||||
|
)
|
||||||
self.lsc = linkedsplitcharts
|
self.lsc = linkedsplitcharts
|
||||||
self.graphics = {}
|
self.graphics = {}
|
||||||
self.plots = []
|
self.plots = []
|
||||||
|
@ -42,12 +53,13 @@ class CrossHair(pg.GraphicsObject):
|
||||||
) -> None:
|
) -> None:
|
||||||
# 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
|
||||||
vl = plot.addLine(x=0, pen=self.pen, movable=False)
|
vl = plot.addLine(x=0, pen=self.lines_pen, movable=False)
|
||||||
hl = plot.addLine(y=0, pen=self.pen, movable=False)
|
hl = plot.addLine(y=0, pen=self.lines_pen, movable=False)
|
||||||
yl = YAxisLabel(
|
yl = YAxisLabel(
|
||||||
parent=plot.getAxis('right'),
|
parent=plot.getAxis('right'),
|
||||||
digits=digits or self.digits,
|
digits=digits or self.digits,
|
||||||
opacity=1
|
opacity=_ch_label_opac,
|
||||||
|
color=self.pen,
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: checkout what ``.sigDelayed`` can be used for
|
# TODO: checkout what ``.sigDelayed`` can be used for
|
||||||
|
@ -55,17 +67,20 @@ class CrossHair(pg.GraphicsObject):
|
||||||
px_moved = pg.SignalProxy(
|
px_moved = pg.SignalProxy(
|
||||||
plot.scene().sigMouseMoved,
|
plot.scene().sigMouseMoved,
|
||||||
rateLimit=_mouse_rate_limit,
|
rateLimit=_mouse_rate_limit,
|
||||||
slot=self.mouseMoved
|
slot=self.mouseMoved,
|
||||||
|
delay=_debounce_delay,
|
||||||
)
|
)
|
||||||
px_enter = pg.SignalProxy(
|
px_enter = pg.SignalProxy(
|
||||||
plot.sig_mouse_enter,
|
plot.sig_mouse_enter,
|
||||||
rateLimit=_mouse_rate_limit,
|
rateLimit=_mouse_rate_limit,
|
||||||
slot=lambda: self.mouseAction('Enter', plot),
|
slot=lambda: self.mouseAction('Enter', plot),
|
||||||
|
delay=_debounce_delay,
|
||||||
)
|
)
|
||||||
px_leave = pg.SignalProxy(
|
px_leave = pg.SignalProxy(
|
||||||
plot.sig_mouse_leave,
|
plot.sig_mouse_leave,
|
||||||
rateLimit=_mouse_rate_limit,
|
rateLimit=_mouse_rate_limit,
|
||||||
slot=lambda: self.mouseAction('Leave', plot),
|
slot=lambda: self.mouseAction('Leave', plot),
|
||||||
|
delay=_debounce_delay,
|
||||||
)
|
)
|
||||||
self.graphics[plot] = {
|
self.graphics[plot] = {
|
||||||
'vl': vl,
|
'vl': vl,
|
||||||
|
@ -75,18 +90,16 @@ class CrossHair(pg.GraphicsObject):
|
||||||
}
|
}
|
||||||
self.plots.append(plot)
|
self.plots.append(plot)
|
||||||
|
|
||||||
# determine where to place x-axis label
|
# Determine where to place x-axis label.
|
||||||
if _xaxis_at == 'bottom':
|
# Place below the last plot by default, ow
|
||||||
# place below the last plot
|
# keep x-axis right below main chart
|
||||||
self.xaxis_label = XAxisLabel(
|
plot_index = -1 if _xaxis_at == 'bottom' else 0
|
||||||
parent=self.plots[-1].getAxis('bottom'),
|
|
||||||
opacity=1
|
self.xaxis_label = XAxisLabel(
|
||||||
)
|
parent=self.plots[plot_index].getAxis('bottom'),
|
||||||
else:
|
opacity=_ch_label_opac,
|
||||||
# keep x-axis right below main chart
|
color=self.pen,
|
||||||
first = self.plots[0]
|
)
|
||||||
xaxis = first.getAxis('bottom')
|
|
||||||
self.xaxis_label = XAxisLabel(parent=xaxis, opacity=1)
|
|
||||||
|
|
||||||
def mouseAction(self, action, plot): # noqa
|
def mouseAction(self, action, plot): # noqa
|
||||||
if action == 'Enter':
|
if action == 'Enter':
|
||||||
|
@ -100,7 +113,10 @@ class CrossHair(pg.GraphicsObject):
|
||||||
self.graphics[plot]['yl'].hide()
|
self.graphics[plot]['yl'].hide()
|
||||||
self.active_plot = None
|
self.active_plot = None
|
||||||
|
|
||||||
def mouseMoved(self, evt): # noqa
|
def mouseMoved(
|
||||||
|
self,
|
||||||
|
evt: 'Tuple[QMouseEvent]', # noqa
|
||||||
|
) -> None: # noqa
|
||||||
"""Update horizonal and vertical lines when mouse moves inside
|
"""Update horizonal and vertical lines when mouse moves inside
|
||||||
either the main chart or any indicator subplot.
|
either the main chart or any indicator subplot.
|
||||||
"""
|
"""
|
||||||
|
@ -108,6 +124,7 @@ class CrossHair(pg.GraphicsObject):
|
||||||
|
|
||||||
# find position inside active plot
|
# find position inside active plot
|
||||||
try:
|
try:
|
||||||
|
# map to view coordinate system
|
||||||
mouse_point = self.active_plot.mapToView(pos)
|
mouse_point = self.active_plot.mapToView(pos)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# mouse was not on active plot
|
# mouse was not on active plot
|
||||||
|
@ -120,7 +137,7 @@ class CrossHair(pg.GraphicsObject):
|
||||||
self.graphics[plot]['hl'].setY(y)
|
self.graphics[plot]['hl'].setY(y)
|
||||||
|
|
||||||
self.graphics[self.active_plot]['yl'].update_label(
|
self.graphics[self.active_plot]['yl'].update_label(
|
||||||
evt_post=pos, point_view=mouse_point
|
abs_pos=pos, data=y
|
||||||
)
|
)
|
||||||
for plot, opts in self.graphics.items():
|
for plot, opts in self.graphics.items():
|
||||||
# move the vertical line to the current x
|
# move the vertical line to the current x
|
||||||
|
@ -131,8 +148,8 @@ class CrossHair(pg.GraphicsObject):
|
||||||
|
|
||||||
# update the label on the bottom of the crosshair
|
# update the label on the bottom of the crosshair
|
||||||
self.xaxis_label.update_label(
|
self.xaxis_label.update_label(
|
||||||
evt_post=pos,
|
abs_pos=pos,
|
||||||
point_view=mouse_point
|
data=x
|
||||||
)
|
)
|
||||||
|
|
||||||
def boundingRect(self):
|
def boundingRect(self):
|
||||||
|
@ -226,7 +243,7 @@ class BarItems(pg.GraphicsObject):
|
||||||
|
|
||||||
# 0.5 is no overlap between arms, 1.0 is full overlap
|
# 0.5 is no overlap between arms, 1.0 is full overlap
|
||||||
w: float = 0.43
|
w: float = 0.43
|
||||||
bull_pen = pg.mkPen('#808080')
|
bull_pen = pg.mkPen(hcolor('gray'))
|
||||||
|
|
||||||
# XXX: tina mode, see below
|
# XXX: tina mode, see below
|
||||||
# bull_brush = pg.mkPen('#00cc00')
|
# bull_brush = pg.mkPen('#00cc00')
|
||||||
|
@ -244,6 +261,9 @@ class BarItems(pg.GraphicsObject):
|
||||||
# track the current length of drawable lines within the larger array
|
# track the current length of drawable lines within the larger array
|
||||||
self.index: int = 0
|
self.index: int = 0
|
||||||
|
|
||||||
|
def last_value(self) -> QLineF:
|
||||||
|
return self.lines[self.index - 1]['rarm']
|
||||||
|
|
||||||
@timeit
|
@timeit
|
||||||
def draw_from_data(
|
def draw_from_data(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -3,6 +3,7 @@ Qt UI styling.
|
||||||
"""
|
"""
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from PyQt5 import QtGui
|
from PyQt5 import QtGui
|
||||||
|
from qdarkstyle.palette import DarkPalette
|
||||||
|
|
||||||
|
|
||||||
# chart-wide font
|
# chart-wide font
|
||||||
|
@ -27,3 +28,36 @@ def enable_tina_mode() -> None:
|
||||||
"""
|
"""
|
||||||
# white background (for tinas like our pal xb)
|
# white background (for tinas like our pal xb)
|
||||||
pg.setConfigOption('background', 'w')
|
pg.setConfigOption('background', 'w')
|
||||||
|
|
||||||
|
|
||||||
|
def hcolor(name: str) -> str:
|
||||||
|
"""Hex color codes by hipster speak.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
# lives matter
|
||||||
|
'black': '#000000',
|
||||||
|
'erie_black': '#1B1B1B',
|
||||||
|
'licorice': '#1A1110',
|
||||||
|
'papas_special': '#06070c',
|
||||||
|
|
||||||
|
# fifty shades
|
||||||
|
'gray': '#808080', # like the kick
|
||||||
|
'jet': '#343434',
|
||||||
|
'charcoal': '#36454F',
|
||||||
|
|
||||||
|
# palette
|
||||||
|
'default': DarkPalette.COLOR_BACKGROUND_NORMAL,
|
||||||
|
|
||||||
|
'white': '#ffffff', # for tinas and sunbathers
|
||||||
|
|
||||||
|
# blue zone
|
||||||
|
'dad_blue': '#326693', # like his shirt
|
||||||
|
'vwap_blue': '#0582fb',
|
||||||
|
'dodger_blue': '#1e90ff', # like the team?
|
||||||
|
'panasonic_blue': '#0040be', # from japan
|
||||||
|
|
||||||
|
# traditional
|
||||||
|
'tina_green': '#00cc00',
|
||||||
|
'tina_red': '#fa0000',
|
||||||
|
|
||||||
|
}[name]
|
||||||
|
|
Loading…
Reference in New Issue