Compare commits

..

No commits in common. "bb9f6475f4494290e2898ee81240c9e110ba2cca" and "649798a91ffc11b4abc5a3c6439e474ba01130da" have entirely different histories.

6 changed files with 82 additions and 201 deletions

View File

@ -1,5 +1,5 @@
# piker: trading gear for hackers. # piker: trading gear for hackers.
# Copyright 2018-forever Tyler Goodlet and p0 # Copyright 2018 Tyler Goodlet
# 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,23 +16,6 @@
""" """
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

View File

@ -142,7 +142,7 @@ class Client:
# durationStr='1 D', # durationStr='1 D',
# time length calcs # time length calcs
durationStr='{count} S'.format(count=1000 * 5), durationStr='{count} S'.format(count=2000 * 5),
barSizeSetting='5 secs', barSizeSetting='5 secs',
# always use extended hours # always use extended hours
@ -225,22 +225,18 @@ 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('.')
# futes # TODO: metadata system for all these exchange rules..
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:
@ -458,8 +454,6 @@ 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],

View File

@ -1,17 +1,12 @@
""" """
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
@ -20,6 +15,7 @@ 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(**{
@ -32,7 +28,10 @@ class PriceAxis(pg.AxisItem):
}) })
self.setLabel(**{'font-size': '10pt'}) self.setLabel(**{'font-size': '10pt'})
self.setTickFont(_font) self.setTickFont(_font)
self.setWidth(125) self.setWidth(150)
# 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
@ -44,20 +43,9 @@ class PriceAxis(pg.AxisItem):
class DynamicDateAxis(pg.AxisItem): class DynamicDateAxis(pg.AxisItem):
# time formats mapped by seconds between bars tick_tpl = {'D1': '%Y-%b-%d'}
tick_tpl = {
60*60*24: '%Y-%b-%d',
60: '%H:%M',
30: '%H:%M:%S',
5: '%H:%M:%S',
}
def __init__( def __init__(self, linked_charts, *args, **kwargs):
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)
@ -71,25 +59,24 @@ class DynamicDateAxis(pg.AxisItem):
) )
# self.setHeight(35) # self.setHeight(35)
def _indexes_to_timestrs( def tickStrings(self, values, scale, spacing):
self, # if len(values) > 1 or not values:
indexes: List[int], # values = Quotes.time
) -> List[str]:
# strings = super().tickStrings(values, scale, spacing)
s_period = 'D1'
strings = []
bars = self.linked_charts.chart._array bars = self.linked_charts.chart._array
times = bars['time'] quotes_count = len(bars) - 1
bars_len = len(bars)
delay = times[-1] - times[times != times[-1]][-1]
epochs = times[list( for ibar in values:
map(int, filter(lambda i: i < bars_len, indexes)) if ibar > quotes_count:
)] return strings
# TODO: **don't** have this hard coded shift to EST dt_tick = fromtimestamp(bars[int(ibar)]['time'])
dts = pd.to_datetime(epochs, unit='s') - 4*pd.offsets.Hour() strings.append(
return dts.strftime(self.tick_tpl[delay]) dt_tick.strftime(self.tick_tpl[s_period])
)
return strings
def tickStrings(self, values: List[float], scale, spacing):
return self._indexes_to_timestrs(values)
class AxisLabel(pg.GraphicsObject): class AxisLabel(pg.GraphicsObject):
@ -101,7 +88,7 @@ class AxisLabel(pg.GraphicsObject):
def __init__( def __init__(
self, self,
parent=None, parent=None,
digits=2, digits=1,
color=None, color=None,
opacity=1, opacity=1,
**kwargs **kwargs
@ -141,7 +128,6 @@ 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()
@ -174,20 +160,24 @@ class XAxisLabel(AxisLabel):
) )
# text_flags = _common_text_flags # text_flags = _common_text_flags
def boundingRect(self): # noqa def tick_to_string(self, tick_pos):
# TODO: we need to get the parent axe's dimensions transformed # TODO: change to actual period
# to abs coords to be 100% correct here: tpl = self.parent.tick_tpl['D1']
# self.parent.boundingRect() bars = self.parent.linked_charts.chart._array
return QtCore.QRectF(0, 0, 100, 31) if tick_pos > len(bars):
return 'Unknown Time'
return fromtimestamp(bars[round(tick_pos)]['time']).strftime(tpl)
def update_label( def boundingRect(self): # noqa
self, return QtCore.QRectF(0, 0, 145, 40)
abs_pos: QPointF, # scene coords
data: float, # data for text def update_label(self, abs_pos, data):
offset: int = 0 # if have margins, k? # ibar = view_pos.x()
) -> None: # if ibar > self.quotes_count:
self.label_str = self.parent._indexes_to_timestrs([int(data)])[0] # return
self.label_str = self.tick_to_string(data)
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)
@ -212,10 +202,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)

View File

@ -15,8 +15,7 @@ from ._axes import (
PriceAxis, PriceAxis,
) )
from ._graphics import CrossHair, BarItems from ._graphics import CrossHair, BarItems
from ._axes import YSticky from ._style import _xaxis_at, _min_points_to_show
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
@ -30,7 +29,7 @@ from .. import fsp
log = get_logger(__name__) log = get_logger(__name__)
# margins # margins
CHART_MARGINS = (0, 0, 5, 3) CHART_MARGINS = (0, 0, 10, 3)
class ChartSpace(QtGui.QWidget): class ChartSpace(QtGui.QWidget):
@ -43,7 +42,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(5, 5, 10, 0) self.toolbar_layout.setContentsMargins(10, 10, 15, 0)
self.h_layout = QtGui.QHBoxLayout() self.h_layout = QtGui.QHBoxLayout()
# self.init_timeframes_ui() # self.init_timeframes_ui()
@ -226,6 +225,7 @@ 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,9 +246,6 @@ 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
@ -275,19 +272,16 @@ 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__( super().__init__(**kwargs)
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')
@ -356,7 +350,6 @@ 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',
@ -380,8 +373,6 @@ 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(
@ -430,21 +421,6 @@ 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,
@ -660,10 +636,6 @@ 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(
@ -674,7 +646,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__()
log.info(f'RECEIVED FIRST QUOTE {quote}') print(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)
@ -703,10 +675,6 @@ 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()

View File

@ -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, hcolor from ._style import _xaxis_at # , _tina_mode
from ._axes import YAxisLabel, XAxisLabel from ._axes import YAxisLabel, XAxisLabel
@ -18,28 +18,17 @@ 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', # noqa linkedsplitcharts: 'LinkedSplitCharts',
digits: int = 0 digits: int = 0
) -> None: ) -> None:
super().__init__() super().__init__()
# XXX: not sure why these are instance variables? self.pen = pg.mkPen('#a9a9a9') # gray?
# 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 = []
@ -53,13 +42,12 @@ 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.lines_pen, movable=False) vl = plot.addLine(x=0, pen=self.pen, movable=False)
hl = plot.addLine(y=0, pen=self.lines_pen, movable=False) hl = plot.addLine(y=0, pen=self.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=_ch_label_opac, opacity=1
color=self.pen,
) )
# TODO: checkout what ``.sigDelayed`` can be used for # TODO: checkout what ``.sigDelayed`` can be used for
@ -67,20 +55,17 @@ 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,
@ -90,16 +75,18 @@ 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
# Place below the last plot by default, ow if _xaxis_at == 'bottom':
# keep x-axis right below main chart # place below the last plot
plot_index = -1 if _xaxis_at == 'bottom' else 0
self.xaxis_label = XAxisLabel( self.xaxis_label = XAxisLabel(
parent=self.plots[plot_index].getAxis('bottom'), parent=self.plots[-1].getAxis('bottom'),
opacity=_ch_label_opac, opacity=1
color=self.pen,
) )
else:
# keep x-axis right below main chart
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':
@ -113,10 +100,7 @@ class CrossHair(pg.GraphicsObject):
self.graphics[plot]['yl'].hide() self.graphics[plot]['yl'].hide()
self.active_plot = None self.active_plot = None
def mouseMoved( def mouseMoved(self, evt): # noqa
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.
""" """
@ -124,7 +108,6 @@ 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
@ -137,7 +120,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(
abs_pos=pos, data=y evt_post=pos, point_view=mouse_point
) )
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
@ -148,8 +131,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(
abs_pos=pos, evt_post=pos,
data=x point_view=mouse_point
) )
def boundingRect(self): def boundingRect(self):
@ -243,7 +226,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(hcolor('gray')) bull_pen = pg.mkPen('#808080')
# XXX: tina mode, see below # XXX: tina mode, see below
# bull_brush = pg.mkPen('#00cc00') # bull_brush = pg.mkPen('#00cc00')
@ -261,9 +244,6 @@ 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,

View File

@ -3,7 +3,6 @@ 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
@ -28,36 +27,3 @@ 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]