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.
# Copyright 2018-forever Tyler Goodlet and p0
# Copyright 2018 Tyler Goodlet
# 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,23 +16,6 @@
"""
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=1000 * 5),
durationStr='{count} S'.format(count=2000 * 5),
barSizeSetting='5 secs',
# always use extended hours
@ -225,22 +225,18 @@ class Client:
# use heuristics to figure out contract "type"
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'):
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:
@ -458,8 +454,6 @@ 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,17 +1,12 @@
"""
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
@ -20,6 +15,7 @@ class PriceAxis(pg.AxisItem):
def __init__(
self,
# chart: 'ChartPlotWidget',
) -> None:
super().__init__(orientation='right')
self.setStyle(**{
@ -32,7 +28,10 @@ class PriceAxis(pg.AxisItem):
})
self.setLabel(**{'font-size': '10pt'})
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
@ -44,20 +43,9 @@ class PriceAxis(pg.AxisItem):
class DynamicDateAxis(pg.AxisItem):
# 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',
}
tick_tpl = {'D1': '%Y-%b-%d'}
def __init__(
self,
linked_charts,
*args,
**kwargs
) -> None:
def __init__(self, linked_charts, *args, **kwargs):
super().__init__(*args, **kwargs)
self.linked_charts = linked_charts
self.setTickFont(_font)
@ -71,25 +59,24 @@ class DynamicDateAxis(pg.AxisItem):
)
# self.setHeight(35)
def _indexes_to_timestrs(
self,
indexes: List[int],
) -> List[str]:
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 = []
bars = self.linked_charts.chart._array
times = bars['time']
bars_len = len(bars)
delay = times[-1] - times[times != times[-1]][-1]
quotes_count = len(bars) - 1
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)
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
class AxisLabel(pg.GraphicsObject):
@ -101,7 +88,7 @@ class AxisLabel(pg.GraphicsObject):
def __init__(
self,
parent=None,
digits=2,
digits=1,
color=None,
opacity=1,
**kwargs
@ -141,7 +128,6 @@ class AxisLabel(pg.GraphicsObject):
p.drawText(option.rect, self.text_flags, self.label_str)
# uggggghhhh
def tick_to_string(self, tick_pos):
raise NotImplementedError()
@ -174,20 +160,24 @@ class XAxisLabel(AxisLabel):
)
# text_flags = _common_text_flags
def boundingRect(self): # noqa
# 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 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 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]
def boundingRect(self): # noqa
return QtCore.QRectF(0, 0, 145, 40)
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)
width = self.boundingRect().width()
offset = 0 # if have margins
new_pos = QPointF(abs_pos.x() - width / 2 - offset, 0)
self.setPos(new_pos)
@ -212,10 +202,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,8 +15,7 @@ from ._axes import (
PriceAxis,
)
from ._graphics import CrossHair, BarItems
from ._axes import YSticky
from ._style import _xaxis_at, _min_points_to_show, hcolor
from ._style import _xaxis_at, _min_points_to_show
from ._source import Symbol
from .. import brokers
from .. import data
@ -30,7 +29,7 @@ from .. import fsp
log = get_logger(__name__)
# margins
CHART_MARGINS = (0, 0, 5, 3)
CHART_MARGINS = (0, 0, 10, 3)
class ChartSpace(QtGui.QWidget):
@ -43,7 +42,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(5, 5, 10, 0)
self.toolbar_layout.setContentsMargins(10, 10, 15, 0)
self.h_layout = QtGui.QHBoxLayout()
# self.init_timeframes_ui()
@ -226,6 +225,7 @@ 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,9 +246,6 @@ class LinkedSplitCharts(QtGui.QWidget):
# scale split regions
self.set_split_sizes()
# XXX: we need this right?
# self.splitter.addWidget(cpw)
return cpw
@ -275,19 +272,16 @@ 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__(
background=hcolor('papas_special'),
# parent=None,
# plotItem=None,
**kwargs
)
super().__init__(**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')
@ -356,7 +350,6 @@ class ChartPlotWidget(pg.PlotWidget):
self._graphics[name] = graphics
# XXX: How to stack labels vertically?
# Ogi says: "
label = pg.LabelItem(
justify='left',
size='5pt',
@ -380,8 +373,6 @@ 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(
@ -430,21 +421,6 @@ 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,
@ -660,10 +636,6 @@ 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(
@ -674,7 +646,7 @@ async def _async_main(
# wait for a first quote before we start any update tasks
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
n.start_soon(add_new_bars, delay, linked_charts)
@ -703,10 +675,6 @@ 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, hcolor
from ._style import _xaxis_at # , _tina_mode
from ._axes import YAxisLabel, XAxisLabel
@ -18,28 +18,17 @@ 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', # noqa
linkedsplitcharts: 'LinkedSplitCharts',
digits: int = 0
) -> None:
super().__init__()
# 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.pen = pg.mkPen('#a9a9a9') # gray?
self.lsc = linkedsplitcharts
self.graphics = {}
self.plots = []
@ -53,13 +42,12 @@ 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.lines_pen, movable=False)
hl = plot.addLine(y=0, pen=self.lines_pen, movable=False)
vl = plot.addLine(x=0, pen=self.pen, movable=False)
hl = plot.addLine(y=0, pen=self.pen, movable=False)
yl = YAxisLabel(
parent=plot.getAxis('right'),
digits=digits or self.digits,
opacity=_ch_label_opac,
color=self.pen,
opacity=1
)
# TODO: checkout what ``.sigDelayed`` can be used for
@ -67,20 +55,17 @@ class CrossHair(pg.GraphicsObject):
px_moved = pg.SignalProxy(
plot.scene().sigMouseMoved,
rateLimit=_mouse_rate_limit,
slot=self.mouseMoved,
delay=_debounce_delay,
slot=self.mouseMoved
)
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,
@ -90,16 +75,18 @@ class CrossHair(pg.GraphicsObject):
}
self.plots.append(plot)
# 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
# determine where to place x-axis label
if _xaxis_at == 'bottom':
# place below the last plot
self.xaxis_label = XAxisLabel(
parent=self.plots[plot_index].getAxis('bottom'),
opacity=_ch_label_opac,
color=self.pen,
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)
def mouseAction(self, action, plot): # noqa
if action == 'Enter':
@ -113,10 +100,7 @@ class CrossHair(pg.GraphicsObject):
self.graphics[plot]['yl'].hide()
self.active_plot = None
def mouseMoved(
self,
evt: 'Tuple[QMouseEvent]', # noqa
) -> None: # noqa
def mouseMoved(self, evt): # noqa
"""Update horizonal and vertical lines when mouse moves inside
either the main chart or any indicator subplot.
"""
@ -124,7 +108,6 @@ 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
@ -137,7 +120,7 @@ class CrossHair(pg.GraphicsObject):
self.graphics[plot]['hl'].setY(y)
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():
# 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
self.xaxis_label.update_label(
abs_pos=pos,
data=x
evt_post=pos,
point_view=mouse_point
)
def boundingRect(self):
@ -243,7 +226,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(hcolor('gray'))
bull_pen = pg.mkPen('#808080')
# XXX: tina mode, see below
# bull_brush = pg.mkPen('#00cc00')
@ -261,9 +244,6 @@ 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,7 +3,6 @@ Qt UI styling.
"""
import pyqtgraph as pg
from PyQt5 import QtGui
from qdarkstyle.palette import DarkPalette
# chart-wide font
@ -28,36 +27,3 @@ 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]