Compare commits

...

10 Commits

Author SHA1 Message Date
Tyler Goodlet bb9f6475f4 Add disclaimer 2020-09-01 12:50:13 -04:00
Tyler Goodlet 54463b3595 Drop to 1k bars on init load 2020-09-01 12:46:30 -04:00
Tyler Goodlet aaf234cbaf Better bg color, tweak margins. 2020-08-31 17:18:35 -04:00
Tyler Goodlet 0f6589d9ff Add proper x-axis time-stamping 2020-08-31 17:18:02 -04:00
Tyler Goodlet 30d8e096c6 Use dashed crosshair, simplify x-axis alloc 2020-08-31 17:17:20 -04:00
Tyler Goodlet 19609178ce Even more colors 2020-08-31 17:16:44 -04:00
Tyler Goodlet 4c39407363 Use dashed lines for crosshair 2020-08-30 12:32:14 -04:00
Tyler Goodlet a345daa522 Try to find cad stocks 2020-08-30 12:31:32 -04:00
Tyler Goodlet ea75281cbc Add and update y-sticky labels on new price data 2020-08-30 12:29:29 -04:00
Tyler Goodlet 86a1f33abb Start color map 2020-08-30 12:28:38 -04:00
6 changed files with 200 additions and 81 deletions

View File

@ -1,5 +1,5 @@
# 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
# it under the terms of the GNU Affero General Public License as published by
@ -16,6 +16,23 @@
"""
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_numpy

View File

@ -142,7 +142,7 @@ class Client:
# durationStr='1 D',
# time length calcs
durationStr='{count} S'.format(count=2000 * 5),
durationStr='{count} S'.format(count=1000 * 5),
barSizeSetting='5 secs',
# always use extended hours
@ -225,18 +225,22 @@ class Client:
# use heuristics to figure out contract "type"
sym, exch = symbol.upper().split('.')
# TODO: metadata system for all these exchange rules..
if exch in ('PURE',):
currency = 'CAD'
# futes
if exch in ('GLOBEX', 'NYMEX', 'CME', 'CMECRYPTO'):
con = await self.get_cont_fute(symbol=sym, exchange=exch)
# commodities
elif exch == 'CMDTY': # eg. XAUUSD.CMDTY
con_kwargs, bars_kwargs = _adhoc_cmdty_data_map[sym]
con = ibis.Commodity(**con_kwargs)
con.bars_kwargs = bars_kwargs
# stonks
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)
try:
@ -454,6 +458,8 @@ def normalize(
return data
# TODO: figure out how to share quote feeds sanely despite
# the wacky ``ib_insync`` api.
# @tractor.msg.pub
async def stream_quotes(
symbols: List[str],

View File

@ -1,12 +1,17 @@
"""
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
from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import QPointF
# from .quantdom.base import Quotes
from .quantdom.utils import fromtimestamp
from ._style import _font, hcolor
@ -15,7 +20,6 @@ class PriceAxis(pg.AxisItem):
def __init__(
self,
# chart: 'ChartPlotWidget',
) -> None:
super().__init__(orientation='right')
self.setStyle(**{
@ -28,10 +32,7 @@ class PriceAxis(pg.AxisItem):
})
self.setLabel(**{'font-size': '10pt'})
self.setTickFont(_font)
self.setWidth(150)
# self.chart = chart
# accesed normally via
# .getAxis('right')
self.setWidth(125)
# XXX: drop for now since it just eats up h space
@ -43,9 +44,20 @@ class PriceAxis(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)
self.linked_charts = linked_charts
self.setTickFont(_font)
@ -59,24 +71,25 @@ class DynamicDateAxis(pg.AxisItem):
)
# self.setHeight(35)
def tickStrings(self, values, scale, spacing):
# if len(values) > 1 or not values:
# values = Quotes.time
# strings = super().tickStrings(values, scale, spacing)
s_period = 'D1'
strings = []
def _indexes_to_timestrs(
self,
indexes: List[int],
) -> List[str]:
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:
if ibar > quotes_count:
return strings
dt_tick = fromtimestamp(bars[int(ibar)]['time'])
strings.append(
dt_tick.strftime(self.tick_tpl[s_period])
)
return strings
epochs = times[list(
map(int, filter(lambda i: i < bars_len, indexes))
)]
# TODO: **don't** have this hard coded shift to EST
dts = pd.to_datetime(epochs, unit='s') - 4*pd.offsets.Hour()
return dts.strftime(self.tick_tpl[delay])
def tickStrings(self, values: List[float], scale, spacing):
return self._indexes_to_timestrs(values)
class AxisLabel(pg.GraphicsObject):
@ -88,7 +101,7 @@ class AxisLabel(pg.GraphicsObject):
def __init__(
self,
parent=None,
digits=1,
digits=2,
color=None,
opacity=1,
**kwargs
@ -128,6 +141,7 @@ class AxisLabel(pg.GraphicsObject):
p.drawText(option.rect, self.text_flags, self.label_str)
# uggggghhhh
def tick_to_string(self, tick_pos):
raise NotImplementedError()
@ -160,24 +174,20 @@ class XAxisLabel(AxisLabel):
)
# 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
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):
# ibar = view_pos.x()
# if ibar > self.quotes_count:
# return
self.label_str = self.tick_to_string(data)
def update_label(
self,
abs_pos: QPointF, # scene coords
data: float, # data for text
offset: int = 0 # if have margins, k?
) -> None:
self.label_str = self.parent._indexes_to_timestrs([int(data)])[0]
width = self.boundingRect().width()
offset = 0 # if have margins
new_pos = QPointF(abs_pos.x() - width / 2 - offset, 0)
self.setPos(new_pos)
@ -202,10 +212,10 @@ class YAxisLabel(AxisLabel):
self,
abs_pos: QPointF, # scene coords
data: float, # data for text
offset: int = 0 # if have margins, k?
) -> None:
self.label_str = self.tick_to_string(data)
height = self.boundingRect().height()
offset = 0 # if have margins
new_pos = QPointF(0, abs_pos.y() - height / 2 - offset)
self.setPos(new_pos)

View File

@ -15,7 +15,8 @@ from ._axes import (
PriceAxis,
)
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 .. import brokers
from .. import data
@ -29,7 +30,7 @@ from .. import fsp
log = get_logger(__name__)
# margins
CHART_MARGINS = (0, 0, 10, 3)
CHART_MARGINS = (0, 0, 5, 3)
class ChartSpace(QtGui.QWidget):
@ -42,7 +43,7 @@ class ChartSpace(QtGui.QWidget):
self.v_layout = QtGui.QVBoxLayout(self)
self.v_layout.setContentsMargins(0, 0, 0, 0)
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.init_timeframes_ui()
@ -225,7 +226,6 @@ class LinkedSplitCharts(QtGui.QWidget):
cpw.setFrameStyle(QtGui.QFrame.StyledPanel | QtGui.QFrame.Plain)
cpw.getPlotItem().setContentsMargins(*CHART_MARGINS)
# self.splitter.addWidget(cpw)
# link chart x-axis to main quotes chart
cpw.setXLink(self.chart)
@ -246,6 +246,9 @@ class LinkedSplitCharts(QtGui.QWidget):
# scale split regions
self.set_split_sizes()
# XXX: we need this right?
# self.splitter.addWidget(cpw)
return cpw
@ -272,16 +275,19 @@ class ChartPlotWidget(pg.PlotWidget):
# the data view we generate graphics from
array: np.ndarray,
**kwargs,
# parent=None,
# background='default',
# plotItem=None,
):
"""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._graphics = {} # registry of underlying graphics
self._labels = {} # registry of underlying graphics
self._ysticks = {} # registry of underlying graphics
# show only right side axes
self.hideAxis('left')
@ -350,6 +356,7 @@ class ChartPlotWidget(pg.PlotWidget):
self._graphics[name] = graphics
# XXX: How to stack labels vertically?
# Ogi says: "
label = pg.LabelItem(
justify='left',
size='5pt',
@ -373,6 +380,8 @@ class ChartPlotWidget(pg.PlotWidget):
# show last 50 points on startup
self.plotItem.vb.setXRange(xlast - 50, xlast + 50)
self._add_sticky(name)
return graphics
def draw_curve(
@ -421,6 +430,21 @@ class ChartPlotWidget(pg.PlotWidget):
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(
self,
name: str,
@ -636,6 +660,10 @@ async def _async_main(
loglevel,
)
# update last price sticky
last = chart._ysticks['last']
last.update_from_data(*chart._array[-1][['index', 'close']])
# graphics update loop
async with data.open_feed(
@ -646,7 +674,7 @@ async def _async_main(
# wait for a first quote before we start any update tasks
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
n.start_soon(add_new_bars, delay, linked_charts)
@ -675,6 +703,10 @@ async def _async_main(
chart.name,
chart._array,
)
# update sticky(s)
last = chart._ysticks['last']
last.update_from_data(
*chart._array[-1][['index', 'close']])
chart._set_yrange()

View File

@ -10,7 +10,7 @@ from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import QLineF
from .quantdom.utils import timeit
from ._style import _xaxis_at # , _tina_mode
from ._style import _xaxis_at, hcolor
from ._axes import YAxisLabel, XAxisLabel
@ -18,17 +18,28 @@ from ._axes import YAxisLabel, XAxisLabel
# - checkout pyqtgraph.PlotCurveItem.setCompositionMode
_mouse_rate_limit = 30
_debounce_delay = 10
_ch_label_opac = 1
class CrossHair(pg.GraphicsObject):
def __init__(
self,
linkedsplitcharts: 'LinkedSplitCharts',
linkedsplitcharts: 'LinkedSplitCharts', # noqa
digits: int = 0
) -> None:
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.graphics = {}
self.plots = []
@ -42,12 +53,13 @@ class CrossHair(pg.GraphicsObject):
) -> None:
# add ``pg.graphicsItems.InfiniteLine``s
# vertical and horizonal lines and a y-axis label
vl = plot.addLine(x=0, pen=self.pen, movable=False)
hl = plot.addLine(y=0, pen=self.pen, movable=False)
vl = plot.addLine(x=0, pen=self.lines_pen, movable=False)
hl = plot.addLine(y=0, pen=self.lines_pen, movable=False)
yl = YAxisLabel(
parent=plot.getAxis('right'),
digits=digits or self.digits,
opacity=1
opacity=_ch_label_opac,
color=self.pen,
)
# TODO: checkout what ``.sigDelayed`` can be used for
@ -55,17 +67,20 @@ class CrossHair(pg.GraphicsObject):
px_moved = pg.SignalProxy(
plot.scene().sigMouseMoved,
rateLimit=_mouse_rate_limit,
slot=self.mouseMoved
slot=self.mouseMoved,
delay=_debounce_delay,
)
px_enter = pg.SignalProxy(
plot.sig_mouse_enter,
rateLimit=_mouse_rate_limit,
slot=lambda: self.mouseAction('Enter', plot),
delay=_debounce_delay,
)
px_leave = pg.SignalProxy(
plot.sig_mouse_leave,
rateLimit=_mouse_rate_limit,
slot=lambda: self.mouseAction('Leave', plot),
delay=_debounce_delay,
)
self.graphics[plot] = {
'vl': vl,
@ -75,18 +90,16 @@ class CrossHair(pg.GraphicsObject):
}
self.plots.append(plot)
# determine where to place x-axis label
if _xaxis_at == 'bottom':
# place below the last plot
self.xaxis_label = XAxisLabel(
parent=self.plots[-1].getAxis('bottom'),
opacity=1
)
else:
# keep x-axis right below main chart
first = self.plots[0]
xaxis = first.getAxis('bottom')
self.xaxis_label = XAxisLabel(parent=xaxis, opacity=1)
# Determine where to place x-axis label.
# Place below the last plot by default, ow
# keep x-axis right below main chart
plot_index = -1 if _xaxis_at == 'bottom' else 0
self.xaxis_label = XAxisLabel(
parent=self.plots[plot_index].getAxis('bottom'),
opacity=_ch_label_opac,
color=self.pen,
)
def mouseAction(self, action, plot): # noqa
if action == 'Enter':
@ -100,7 +113,10 @@ class CrossHair(pg.GraphicsObject):
self.graphics[plot]['yl'].hide()
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
either the main chart or any indicator subplot.
"""
@ -108,6 +124,7 @@ class CrossHair(pg.GraphicsObject):
# find position inside active plot
try:
# map to view coordinate system
mouse_point = self.active_plot.mapToView(pos)
except AttributeError:
# mouse was not on active plot
@ -120,7 +137,7 @@ class CrossHair(pg.GraphicsObject):
self.graphics[plot]['hl'].setY(y)
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():
# 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
self.xaxis_label.update_label(
evt_post=pos,
point_view=mouse_point
abs_pos=pos,
data=x
)
def boundingRect(self):
@ -226,7 +243,7 @@ class BarItems(pg.GraphicsObject):
# 0.5 is no overlap between arms, 1.0 is full overlap
w: float = 0.43
bull_pen = pg.mkPen('#808080')
bull_pen = pg.mkPen(hcolor('gray'))
# XXX: tina mode, see below
# bull_brush = pg.mkPen('#00cc00')
@ -244,6 +261,9 @@ class BarItems(pg.GraphicsObject):
# track the current length of drawable lines within the larger array
self.index: int = 0
def last_value(self) -> QLineF:
return self.lines[self.index - 1]['rarm']
@timeit
def draw_from_data(
self,

View File

@ -3,6 +3,7 @@ Qt UI styling.
"""
import pyqtgraph as pg
from PyQt5 import QtGui
from qdarkstyle.palette import DarkPalette
# chart-wide font
@ -27,3 +28,36 @@ def enable_tina_mode() -> None:
"""
# white background (for tinas like our pal xb)
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]