commit
84f357b7eb
17
README.rst
17
README.rst
|
@ -1,14 +1,12 @@
|
||||||
piker
|
piker
|
||||||
-----
|
-----
|
||||||
Anti-fragile_ trading gear for hackers, scientists, stay-at-home quants and underpants warriors.
|
Trading gear for hackers.
|
||||||
|
|
||||||
|pypi| |travis| |versions| |license| |docs|
|
|pypi| |travis| |versions| |license| |docs|
|
||||||
|
|
||||||
.. |travis| image:: https://img.shields.io/travis/pikers/piker/master.svg
|
.. |travis| image:: https://img.shields.io/travis/pikers/piker/master.svg
|
||||||
:target: https://travis-ci.org/pikers/piker
|
:target: https://travis-ci.org/pikers/piker
|
||||||
|
|
||||||
.. _Anti-fragile: https://www.sciencedirect.com/science/article/pii/S1877050916302290
|
|
||||||
|
|
||||||
Install
|
Install
|
||||||
*******
|
*******
|
||||||
``piker`` is currently under heavy alpha development and as such should
|
``piker`` is currently under heavy alpha development and as such should
|
||||||
|
@ -25,7 +23,7 @@ For a development install::
|
||||||
pipenv install --dev -e .
|
pipenv install --dev -e .
|
||||||
pipenv shell
|
pipenv shell
|
||||||
|
|
||||||
To start the real-time index ETF watchlist::
|
To start the real-time index ETF watchlist with the `robinhood` backend::
|
||||||
|
|
||||||
piker watch indexes -l info
|
piker watch indexes -l info
|
||||||
|
|
||||||
|
@ -50,7 +48,7 @@ Then start the client app as normal::
|
||||||
|
|
||||||
Laggy distros
|
Laggy distros
|
||||||
=============
|
=============
|
||||||
For those running pop-culture distros that don't yet ship ``python3.6``
|
For those running pop-culture distros that don't yet ship ``python3.7``
|
||||||
you'll need to install it as well as `kivy source build`_ dependencies
|
you'll need to install it as well as `kivy source build`_ dependencies
|
||||||
since currently there's reliance on an async development branch.
|
since currently there's reliance on an async development branch.
|
||||||
|
|
||||||
|
@ -60,5 +58,10 @@ since currently there's reliance on an async development branch.
|
||||||
Tech
|
Tech
|
||||||
****
|
****
|
||||||
``piker`` is an attempt at a pro-grade, next-gen open source toolset
|
``piker`` is an attempt at a pro-grade, next-gen open source toolset
|
||||||
for trading and financial analysis. As such, it tries to use as much
|
for real-time trading and financial analysis.
|
||||||
cutting edge tech as possible including Python 3.6+ and ``trio``.
|
|
||||||
|
It tries to use as much cutting edge tech as possible including (but not limited to):
|
||||||
|
|
||||||
|
- Python 3.7+
|
||||||
|
- ``trio``
|
||||||
|
- ``tractor``
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
"""Hoverable Behaviour (changing when the mouse is on the widget by O. Poyen.
|
||||||
|
License: LGPL
|
||||||
|
"""
|
||||||
|
__author__ = 'Olivier Poyen'
|
||||||
|
|
||||||
|
|
||||||
|
from kivy.properties import BooleanProperty, ObjectProperty
|
||||||
|
from kivy.factory import Factory
|
||||||
|
from kivy.core.window import Window
|
||||||
|
|
||||||
|
|
||||||
|
class HoverBehavior(object):
|
||||||
|
"""Hover behavior.
|
||||||
|
|
||||||
|
:Events:
|
||||||
|
`on_enter`
|
||||||
|
Fired when mouse enter the bbox of the widget.
|
||||||
|
`on_leave`
|
||||||
|
Fired when the mouse exit the widget.
|
||||||
|
"""
|
||||||
|
hovered = BooleanProperty(False)
|
||||||
|
# Contains the last relevant point received by the Hoverable. This can
|
||||||
|
# be used in `on_enter` or `on_leave` in order to know where was dispatched
|
||||||
|
# the event.
|
||||||
|
border_point = ObjectProperty(None)
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.register_event_type('on_enter')
|
||||||
|
self.register_event_type('on_leave')
|
||||||
|
Window.bind(mouse_pos=self.on_mouse_pos)
|
||||||
|
super(HoverBehavior, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def on_mouse_pos(self, *args):
|
||||||
|
# don't proceed if I'm not displayed <=> If have no parent
|
||||||
|
if not self.get_root_window():
|
||||||
|
return
|
||||||
|
pos = args[1]
|
||||||
|
# Next line to_widget allow to compensate for relative layout
|
||||||
|
inside = self.collide_point(*self.to_widget(*pos))
|
||||||
|
if self.hovered == inside:
|
||||||
|
# We have already done what was needed
|
||||||
|
return
|
||||||
|
self.border_point = pos
|
||||||
|
self.hovered = inside
|
||||||
|
if inside:
|
||||||
|
self.dispatch('on_enter')
|
||||||
|
else:
|
||||||
|
self.dispatch('on_leave')
|
||||||
|
|
||||||
|
# implement these in the widget impl
|
||||||
|
|
||||||
|
def on_enter(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_leave(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# register for global use via kivy.factory.Factory
|
||||||
|
Factory.register('HoverBehavior', HoverBehavior)
|
|
@ -7,8 +7,10 @@ Launch with ``piker monitor <watchlist name>``.
|
||||||
"""
|
"""
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from types import ModuleType, AsyncGeneratorType
|
from types import ModuleType, AsyncGeneratorType
|
||||||
|
from typing import List
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
|
import tractor
|
||||||
from kivy.uix.boxlayout import BoxLayout
|
from kivy.uix.boxlayout import BoxLayout
|
||||||
from kivy.uix.gridlayout import GridLayout
|
from kivy.uix.gridlayout import GridLayout
|
||||||
from kivy.uix.stacklayout import StackLayout
|
from kivy.uix.stacklayout import StackLayout
|
||||||
|
@ -20,6 +22,7 @@ from kivy.core.window import Window
|
||||||
|
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from .pager import PagerView
|
from .pager import PagerView
|
||||||
|
from .kivy.hoverable import HoverBehavior
|
||||||
|
|
||||||
log = get_logger('monitor')
|
log = get_logger('monitor')
|
||||||
|
|
||||||
|
@ -41,41 +44,63 @@ def colorcode(name):
|
||||||
return _colors[name if name else 'gray']
|
return _colors[name if name else 'gray']
|
||||||
|
|
||||||
|
|
||||||
_bs = 4 # border size
|
_bs = 0.75 # border size
|
||||||
_color = [0.13]*3 # nice shade of gray
|
|
||||||
|
# medium shade of gray that seems to match the
|
||||||
|
# default i3 window borders
|
||||||
|
_i3_rgba = [0.14]*3 + [1]
|
||||||
|
|
||||||
|
# slightly off black like the jellybean bg from
|
||||||
|
# vim colorscheme
|
||||||
|
_cell_rgba = [0.07]*3 + [1]
|
||||||
|
_black_rgba = [0]*4
|
||||||
|
|
||||||
_kv = (f'''
|
_kv = (f'''
|
||||||
#:kivy 1.10.0
|
#:kivy 1.10.0
|
||||||
|
|
||||||
<Cell>
|
<Cell>
|
||||||
font_size: 20
|
font_size: 21
|
||||||
# text_size: self.size
|
# make text wrap to botom
|
||||||
size: self.texture_size
|
text_size: self.size
|
||||||
color: {colorcode('gray')}
|
|
||||||
font_color: {colorcode('gray')}
|
|
||||||
font_name: 'Roboto-Regular'
|
|
||||||
background_color: [0.13]*3 + [1]
|
|
||||||
background_normal: ''
|
|
||||||
valign: 'middle'
|
|
||||||
halign: 'center'
|
halign: 'center'
|
||||||
# outline_color: [0.1]*4
|
valign: 'middle'
|
||||||
|
size: self.texture_size
|
||||||
|
# color: {colorcode('gray')}
|
||||||
|
# font_color: {colorcode('gray')}
|
||||||
|
font_name: 'Roboto-Regular'
|
||||||
|
background_color: [0]*4 # by default transparent; use row color
|
||||||
|
# background_color: {_cell_rgba}
|
||||||
|
# spacing: 0, 0
|
||||||
|
# padding: [0]*4
|
||||||
|
|
||||||
<HeaderCell>
|
<HeaderCell>
|
||||||
font_size: 20
|
font_size: 21
|
||||||
background_color: [0]*4
|
background_color: [0]*4 # by default transparent; use row color
|
||||||
canvas.before:
|
# background_color: {_cell_rgba}
|
||||||
Color:
|
# canvas.before:
|
||||||
rgb: {_color}
|
# Color:
|
||||||
BorderImage: # use a fixed size border
|
# rgba: [0.13]*4
|
||||||
pos: self.pos
|
# BorderImage: # use a fixed size border
|
||||||
size: [self.size[0] - {_bs}, self.size[1]]
|
# pos: self.pos
|
||||||
# 0s are because the containing TickerTable already has spacing
|
# size: [self.size[0] - {_bs}, self.size[1]]
|
||||||
border: [0, {_bs} , 0, {_bs}]
|
# # 0s are because the containing TickerTable already has spacing
|
||||||
|
# # border: [0, {_bs} , 0, {_bs}]
|
||||||
|
# border: [0, {_bs} , 0, 0]
|
||||||
|
|
||||||
<TickerTable>
|
<TickerTable>
|
||||||
spacing: '{_bs}dp'
|
spacing: [{_bs}]
|
||||||
row_force_default: True
|
# row_force_default: True
|
||||||
row_default_height: 63
|
row_default_height: 62
|
||||||
cols: 1
|
cols: 1
|
||||||
|
canvas.before:
|
||||||
|
Color:
|
||||||
|
# i3 style gray as background
|
||||||
|
rgba: {_i3_rgba}
|
||||||
|
# rgba: {_cell_rgba}
|
||||||
|
Rectangle:
|
||||||
|
# scale with container self here refers to the widget i.e BoxLayout
|
||||||
|
pos: self.pos
|
||||||
|
size: self.size
|
||||||
|
|
||||||
<BidAskLayout>
|
<BidAskLayout>
|
||||||
spacing: [{_bs}, 0]
|
spacing: [{_bs}, 0]
|
||||||
|
@ -84,18 +109,48 @@ _kv = (f'''
|
||||||
# minimum_height: 200 # should be pulled from Cell text size
|
# minimum_height: 200 # should be pulled from Cell text size
|
||||||
# minimum_width: 200
|
# minimum_width: 200
|
||||||
# row_force_default: True
|
# row_force_default: True
|
||||||
# row_default_height: 75
|
# row_default_height: 61 # determines the header row size
|
||||||
# outline_color: [.7]*4
|
padding: [0]*4
|
||||||
|
spacing: [0]
|
||||||
|
canvas.before:
|
||||||
|
Color:
|
||||||
|
# rgba: [0]*4
|
||||||
|
rgba: {_cell_rgba}
|
||||||
|
Rectangle:
|
||||||
|
# self here refers to the widget i.e Row(GridLayout)
|
||||||
|
pos: self.pos
|
||||||
|
size: self.size
|
||||||
|
# row higlighting on mouse over
|
||||||
|
Color:
|
||||||
|
rgba: {_i3_rgba}
|
||||||
|
RoundedRectangle:
|
||||||
|
size: self.width, self.height if self.hovered else 1
|
||||||
|
pos: self.pos
|
||||||
|
radius: (10,)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<SearchBar>
|
|
||||||
# part of the `PagerView`
|
# part of the `PagerView`
|
||||||
size_hint: 1, 0.03
|
<SearchBar>
|
||||||
|
size_hint: 1, None
|
||||||
|
# static size of 51 px
|
||||||
|
height: 51
|
||||||
font_size: 25
|
font_size: 25
|
||||||
background_color: [0.13]*3 + [1]
|
background_color: {_i3_rgba}
|
||||||
''')
|
''')
|
||||||
|
|
||||||
|
|
||||||
class HeaderCell(Button):
|
class Cell(Button):
|
||||||
|
"""Data cell: the fundemental widget.
|
||||||
|
|
||||||
|
``key`` is the column name index value.
|
||||||
|
"""
|
||||||
|
def __init__(self, key=None, **kwargs):
|
||||||
|
super(Cell, self).__init__(**kwargs)
|
||||||
|
self.key = key
|
||||||
|
|
||||||
|
|
||||||
|
class HeaderCell(Cell):
|
||||||
"""Column header cell label.
|
"""Column header cell label.
|
||||||
"""
|
"""
|
||||||
def on_press(self, value=None):
|
def on_press(self, value=None):
|
||||||
|
@ -103,6 +158,7 @@ class HeaderCell(Button):
|
||||||
in `update_quotes()`.
|
in `update_quotes()`.
|
||||||
"""
|
"""
|
||||||
table = self.row.table
|
table = self.row.table
|
||||||
|
# if this is a row header cell then sort by the clicked field
|
||||||
if self.row.is_header:
|
if self.row.is_header:
|
||||||
table.sort_key = self.key
|
table.sort_key = self.key
|
||||||
|
|
||||||
|
@ -114,7 +170,7 @@ class HeaderCell(Button):
|
||||||
# outline the header text to indicate it's been the last clicked
|
# outline the header text to indicate it's been the last clicked
|
||||||
self.underline = True
|
self.underline = True
|
||||||
self.bold = True
|
self.bold = True
|
||||||
# mark this cell as the last
|
# mark this cell as the last selected
|
||||||
table.last_clicked_col_cell = self
|
table.last_clicked_col_cell = self
|
||||||
# sort and render the rows immediately
|
# sort and render the rows immediately
|
||||||
self.row.table.render_rows(table.quote_cache)
|
self.row.table.render_rows(table.quote_cache)
|
||||||
|
@ -122,39 +178,40 @@ class HeaderCell(Button):
|
||||||
# allow highlighting of row headers for tracking
|
# allow highlighting of row headers for tracking
|
||||||
elif self.is_header:
|
elif self.is_header:
|
||||||
if self.background_color == self.color:
|
if self.background_color == self.color:
|
||||||
self.background_color = [0]*4
|
self.background_color = _black_rgba
|
||||||
else:
|
else:
|
||||||
self.background_color = self.color
|
self.background_color = self.color
|
||||||
|
|
||||||
|
|
||||||
class Cell(Button):
|
|
||||||
"""Data cell.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class BidAskLayout(StackLayout):
|
class BidAskLayout(StackLayout):
|
||||||
"""Cell which houses three buttons containing a last, bid, and ask in a
|
"""Cell which houses three buttons containing a last, bid, and ask in a
|
||||||
single unit oriented with the last 2 under the first.
|
single unit oriented with the last 2 under the first.
|
||||||
"""
|
"""
|
||||||
def __init__(self, values, header=False, **kwargs):
|
def __init__(self, values, header=False, **kwargs):
|
||||||
|
# uncomment to get vertical stacked bid-ask
|
||||||
|
# super(BidAskLayout, self).__init__(orientation='bt-lr', **kwargs)
|
||||||
super(BidAskLayout, self).__init__(orientation='lr-tb', **kwargs)
|
super(BidAskLayout, self).__init__(orientation='lr-tb', **kwargs)
|
||||||
assert len(values) == 3, "You can only provide 3 values: last,bid,ask"
|
assert len(values) == 3, "You can only provide 3 values: last,bid,ask"
|
||||||
self._keys2cells = {}
|
self._keys2cells = {}
|
||||||
cell_type = HeaderCell if header else Cell
|
cell_type = HeaderCell if header else Cell
|
||||||
top_size = cell_type().font_size
|
top_size = cell_type().font_size
|
||||||
small_size = top_size - 2
|
small_size = top_size - 4
|
||||||
top_prop = 0.55 # proportion of size used by top cell
|
top_prop = 0.5 # proportion of size used by top cell
|
||||||
bottom_prop = 1 - top_prop
|
bottom_prop = 1 - top_prop
|
||||||
for (key, size_hint, font_size), value in zip(
|
for (key, size_hint, font_size), value in zip(
|
||||||
[('last', (1, top_prop), top_size),
|
[('last', (1, top_prop), top_size),
|
||||||
('bid', (0.5, bottom_prop), small_size),
|
('bid', (0.5, bottom_prop), small_size),
|
||||||
('ask', (0.5, bottom_prop), small_size)],
|
('ask', (0.5, bottom_prop), small_size)],
|
||||||
|
# uncomment to get vertical stacked bid-ask
|
||||||
|
# [('last', (top_prop, 1), top_size),
|
||||||
|
# ('bid', (bottom_prop, 0.5), small_size),
|
||||||
|
# ('ask', (bottom_prop, 0.5), small_size)],
|
||||||
values
|
values
|
||||||
):
|
):
|
||||||
cell = cell_type(
|
cell = cell_type(
|
||||||
text=str(value),
|
text=str(value),
|
||||||
size_hint=size_hint,
|
size_hint=size_hint,
|
||||||
width=self.width/2 - 3,
|
# width=self.width/2 - 3,
|
||||||
font_size=font_size
|
font_size=font_size
|
||||||
)
|
)
|
||||||
self._keys2cells[key] = cell
|
self._keys2cells[key] = cell
|
||||||
|
@ -184,7 +241,7 @@ class BidAskLayout(StackLayout):
|
||||||
return [self.last, self.bid, self.ask]
|
return [self.last, self.bid, self.ask]
|
||||||
|
|
||||||
|
|
||||||
class Row(GridLayout):
|
class Row(GridLayout, HoverBehavior):
|
||||||
"""A grid for displaying a row of ticker quote data.
|
"""A grid for displaying a row of ticker quote data.
|
||||||
|
|
||||||
The row fields can be updated using the ``fields`` property which will in
|
The row fields can be updated using the ``fields`` property which will in
|
||||||
|
@ -192,14 +249,17 @@ class Row(GridLayout):
|
||||||
"""
|
"""
|
||||||
def __init__(
|
def __init__(
|
||||||
self, record, headers=(), bidasks=None, table=None,
|
self, record, headers=(), bidasks=None, table=None,
|
||||||
is_header_row=False,
|
is_header=False,
|
||||||
**kwargs
|
**kwargs
|
||||||
):
|
):
|
||||||
super(Row, self).__init__(cols=len(record), **kwargs)
|
super(Row, self).__init__(cols=len(record), **kwargs)
|
||||||
self._cell_widgets = {}
|
self._cell_widgets = {}
|
||||||
self._last_record = record
|
self._last_record = record
|
||||||
self.table = table
|
self.table = table
|
||||||
self.is_header = is_header_row
|
self.is_header = is_header
|
||||||
|
|
||||||
|
# selection state
|
||||||
|
self.mouse_over = False
|
||||||
|
|
||||||
# create `BidAskCells` first
|
# create `BidAskCells` first
|
||||||
layouts = {}
|
layouts = {}
|
||||||
|
@ -208,7 +268,7 @@ class Row(GridLayout):
|
||||||
for key, children in bidasks.items():
|
for key, children in bidasks.items():
|
||||||
layout = BidAskLayout(
|
layout = BidAskLayout(
|
||||||
[record[key]] + [record[child] for child in children],
|
[record[key]] + [record[child] for child in children],
|
||||||
header=is_header_row
|
header=is_header
|
||||||
)
|
)
|
||||||
layout.row = self
|
layout.row = self
|
||||||
layouts[key] = layout
|
layouts[key] = layout
|
||||||
|
@ -229,41 +289,68 @@ class Row(GridLayout):
|
||||||
# these cells have already been added to the `BidAskLayout`
|
# these cells have already been added to the `BidAskLayout`
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
cell = self._append_cell(val, header=header)
|
cell = self._append_cell(val, key, header=header)
|
||||||
cell.key = key
|
cell.key = key
|
||||||
self._cell_widgets[key] = cell
|
self._cell_widgets[key] = cell
|
||||||
|
|
||||||
def get_cell(self, key):
|
def get_cell(self, key):
|
||||||
return self._cell_widgets[key]
|
return self._cell_widgets[key]
|
||||||
|
|
||||||
def _append_cell(self, text, header=False):
|
def _append_cell(self, text, key, header=False):
|
||||||
if not len(self._cell_widgets) < self.cols:
|
if not len(self._cell_widgets) < self.cols:
|
||||||
raise ValueError(f"Can not append more then {self.cols} cells")
|
raise ValueError(f"Can not append more then {self.cols} cells")
|
||||||
|
|
||||||
# header cells just have a different colour
|
# header cells just have a different colour
|
||||||
celltype = HeaderCell if header else Cell
|
celltype = HeaderCell if header else Cell
|
||||||
cell = celltype(text=str(text))
|
cell = celltype(text=str(text), key=key)
|
||||||
cell.is_header = header
|
cell.is_header = header
|
||||||
cell.row = self
|
cell.row = self
|
||||||
self.add_widget(cell)
|
self.add_widget(cell)
|
||||||
return cell
|
return cell
|
||||||
|
|
||||||
def update(self, record, displayable):
|
def update(self, record, displayable):
|
||||||
|
"""Update this row's cells with new values from a quote ``record``.
|
||||||
|
|
||||||
|
Return all cells that changed in a ``dict``.
|
||||||
|
"""
|
||||||
# color changed field values
|
# color changed field values
|
||||||
|
cells = {}
|
||||||
|
gray = colorcode('gray')
|
||||||
|
fgreen = colorcode('forestgreen')
|
||||||
|
red = colorcode('red2')
|
||||||
for key, val in record.items():
|
for key, val in record.items():
|
||||||
# logic for cell text coloring: up-green, down-red
|
# logic for cell text coloring: up-green, down-red
|
||||||
if self._last_record[key] < val:
|
if self._last_record[key] < val:
|
||||||
color = colorcode('forestgreen')
|
color = fgreen
|
||||||
elif self._last_record[key] > val:
|
elif self._last_record[key] > val:
|
||||||
color = colorcode('red2')
|
color = red
|
||||||
else:
|
else:
|
||||||
color = colorcode('gray')
|
color = gray
|
||||||
|
|
||||||
cell = self.get_cell(key)
|
cell = self.get_cell(key)
|
||||||
cell.text = str(displayable[key])
|
cell.text = str(displayable[key])
|
||||||
cell.color = color
|
cell.color = color
|
||||||
|
if color != gray:
|
||||||
|
cells[key] = cell
|
||||||
|
|
||||||
self._last_record = record
|
self._last_record = record
|
||||||
|
return cells
|
||||||
|
|
||||||
|
# mouse over handlers
|
||||||
|
def on_enter(self):
|
||||||
|
"""Highlight layout on enter.
|
||||||
|
"""
|
||||||
|
log.debug(
|
||||||
|
f"Entered row {type(self)} through {self.border_point}")
|
||||||
|
# don't highlight header row
|
||||||
|
if getattr(self, 'is_header', None):
|
||||||
|
self.hovered = False
|
||||||
|
|
||||||
|
def on_leave(self):
|
||||||
|
"""Un-highlight layout on exit.
|
||||||
|
"""
|
||||||
|
log.debug(
|
||||||
|
f"Left row {type(self)} through {self.border_point}")
|
||||||
|
|
||||||
|
|
||||||
class TickerTable(GridLayout):
|
class TickerTable(GridLayout):
|
||||||
|
@ -277,6 +364,7 @@ class TickerTable(GridLayout):
|
||||||
self.row_filter = lambda item: item
|
self.row_filter = lambda item: item
|
||||||
# for tracking last clicked column header cell
|
# for tracking last clicked column header cell
|
||||||
self.last_clicked_col_cell = None
|
self.last_clicked_col_cell = None
|
||||||
|
self._last_row_toggle = 0
|
||||||
|
|
||||||
def append_row(self, record, bidasks=None):
|
def append_row(self, record, bidasks=None):
|
||||||
"""Append a `Row` of `Cell` objects to this table.
|
"""Append a `Row` of `Cell` objects to this table.
|
||||||
|
@ -288,7 +376,8 @@ class TickerTable(GridLayout):
|
||||||
return row
|
return row
|
||||||
|
|
||||||
def render_rows(
|
def render_rows(
|
||||||
self, pairs: {str: (dict, Row)}, sort_key: str = None, row_filter=None,
|
self, pairs: {str: (dict, Row)}, sort_key: str = None,
|
||||||
|
row_filter=None,
|
||||||
):
|
):
|
||||||
"""Sort and render all rows on the ticker grid from ``pairs``.
|
"""Sort and render all rows on the ticker grid from ``pairs``.
|
||||||
"""
|
"""
|
||||||
|
@ -317,7 +406,7 @@ class TickerTable(GridLayout):
|
||||||
|
|
||||||
|
|
||||||
async def update_quotes(
|
async def update_quotes(
|
||||||
nursery: 'Nursery',
|
nursery: trio._core._run.Nursery,
|
||||||
brokermod: ModuleType,
|
brokermod: ModuleType,
|
||||||
widgets: dict,
|
widgets: dict,
|
||||||
agen: AsyncGeneratorType,
|
agen: AsyncGeneratorType,
|
||||||
|
@ -326,11 +415,19 @@ async def update_quotes(
|
||||||
):
|
):
|
||||||
"""Process live quotes by updating ticker rows.
|
"""Process live quotes by updating ticker rows.
|
||||||
"""
|
"""
|
||||||
grid = widgets['grid']
|
table = widgets['table']
|
||||||
|
flash_keys = {'low', 'high'}
|
||||||
|
|
||||||
def color_row(row, data):
|
async def revert_cells_color(cells):
|
||||||
|
await trio.sleep(0.3)
|
||||||
|
for cell in cells:
|
||||||
|
cell.background_color = _black_rgba
|
||||||
|
|
||||||
|
def color_row(row, data, cells):
|
||||||
hdrcell = row.get_cell('symbol')
|
hdrcell = row.get_cell('symbol')
|
||||||
chngcell = row.get_cell('%')
|
chngcell = row.get_cell('%')
|
||||||
|
|
||||||
|
# determine daily change color
|
||||||
daychange = float(data['%'])
|
daychange = float(data['%'])
|
||||||
if daychange < 0.:
|
if daychange < 0.:
|
||||||
color = colorcode('red2')
|
color = colorcode('red2')
|
||||||
|
@ -339,49 +436,86 @@ async def update_quotes(
|
||||||
else:
|
else:
|
||||||
color = colorcode('gray')
|
color = colorcode('gray')
|
||||||
|
|
||||||
# row header and % cell color
|
# update row header and '%' cell text color
|
||||||
chngcell.color = hdrcell.color = color
|
chngcell.color = hdrcell.color = color
|
||||||
# bgcolor = color.copy()
|
|
||||||
# bgcolor[-1] = 0.25
|
|
||||||
# chngcell.background_color = bgcolor
|
|
||||||
|
|
||||||
# if the cell has been "highlighted" make sure to change its color
|
# if the cell has been "highlighted" make sure to change its color
|
||||||
if hdrcell.background_color != [0]*4:
|
if hdrcell.background_color != [0]*4:
|
||||||
hdrcell.background_color = color
|
hdrcell.background_color = color
|
||||||
|
|
||||||
|
# briefly highlight bg of certain cells on each trade execution
|
||||||
|
unflash = set()
|
||||||
|
tick_color = None
|
||||||
|
last = cells.get('last')
|
||||||
|
if not last:
|
||||||
|
vol = cells.get('vol')
|
||||||
|
if not vol:
|
||||||
|
return # no trade exec took place
|
||||||
|
|
||||||
|
# flash gray on volume tick
|
||||||
|
# (means trade exec @ current price)
|
||||||
|
last = row.get_cell('last')
|
||||||
|
tick_color = colorcode('gray')
|
||||||
|
else:
|
||||||
|
tick_color = last.color
|
||||||
|
|
||||||
|
last.background_color = tick_color
|
||||||
|
unflash.add(last)
|
||||||
|
# flash the size cell
|
||||||
|
size = row.get_cell('size')
|
||||||
|
size.background_color = tick_color
|
||||||
|
unflash.add(size)
|
||||||
|
|
||||||
|
# flash all other cells
|
||||||
|
for key in flash_keys:
|
||||||
|
cell = cells.get(key)
|
||||||
|
if cell:
|
||||||
|
cell.background_color = cell.color
|
||||||
|
unflash.add(cell)
|
||||||
|
|
||||||
|
# revert flash state momentarily
|
||||||
|
nursery.start_soon(revert_cells_color, unflash)
|
||||||
|
|
||||||
cache = {}
|
cache = {}
|
||||||
grid.quote_cache = cache
|
table.quote_cache = cache
|
||||||
|
|
||||||
# initial coloring
|
# initial coloring
|
||||||
for sym, quote in first_quotes.items():
|
for sym, quote in first_quotes.items():
|
||||||
row = grid.symbols2rows[sym]
|
row = table.symbols2rows[sym]
|
||||||
record, displayable = brokermod.format_quote(
|
record, displayable = brokermod.format_quote(
|
||||||
quote, symbol_data=symbol_data)
|
quote, symbol_data=symbol_data)
|
||||||
row.update(record, displayable)
|
row.update(record, displayable)
|
||||||
color_row(row, record)
|
color_row(row, record, {})
|
||||||
cache[sym] = (record, row)
|
cache[sym] = (record, row)
|
||||||
|
|
||||||
# render all rows once up front
|
# render all rows once up front
|
||||||
grid.render_rows(cache)
|
table.render_rows(cache)
|
||||||
|
|
||||||
# real-time cell update loop
|
# real-time cell update loop
|
||||||
async for quotes in agen: # new quotes data only
|
async for quotes in agen: # new quotes data only
|
||||||
for symbol, quote in quotes.items():
|
for symbol, quote in quotes.items():
|
||||||
record, displayable = brokermod.format_quote(
|
record, displayable = brokermod.format_quote(
|
||||||
quote, symbol_data=symbol_data)
|
quote, symbol_data=symbol_data)
|
||||||
row = grid.symbols2rows[symbol]
|
row = table.symbols2rows[symbol]
|
||||||
cache[symbol] = (record, row)
|
cache[symbol] = (record, row)
|
||||||
row.update(record, displayable)
|
cells = row.update(record, displayable)
|
||||||
color_row(row, record)
|
color_row(row, record, cells)
|
||||||
|
|
||||||
grid.render_rows(cache)
|
table.render_rows(cache)
|
||||||
log.debug("Waiting on quotes")
|
log.debug("Waiting on quotes")
|
||||||
|
|
||||||
log.warn("`brokerd` connection dropped")
|
log.warn("Data feed connection dropped")
|
||||||
nursery.cancel_scope.cancel()
|
nursery.cancel_scope.cancel()
|
||||||
|
|
||||||
|
|
||||||
async def _async_main(name, portal, tickers, brokermod, rate):
|
async def _async_main(
|
||||||
|
name: str,
|
||||||
|
portal: tractor._portal.Portal,
|
||||||
|
tickers: List[str],
|
||||||
|
brokermod: ModuleType,
|
||||||
|
rate: int,
|
||||||
|
# an async generator instance which yields quotes dict packets
|
||||||
|
quote_gen: AsyncGeneratorType,
|
||||||
|
) -> None:
|
||||||
'''Launch kivy app + all other related tasks.
|
'''Launch kivy app + all other related tasks.
|
||||||
|
|
||||||
This is started with cli cmd `piker monitor`.
|
This is started with cli cmd `piker monitor`.
|
||||||
|
@ -389,18 +523,13 @@ async def _async_main(name, portal, tickers, brokermod, rate):
|
||||||
# subscribe for tickers (this performs a possible filtering
|
# subscribe for tickers (this performs a possible filtering
|
||||||
# where invalid symbols are discarded)
|
# where invalid symbols are discarded)
|
||||||
sd = await portal.run(
|
sd = await portal.run(
|
||||||
"piker.brokers.core", 'symbol_data',
|
"piker.brokers.data", 'symbol_data',
|
||||||
broker=brokermod.name, tickers=tickers)
|
|
||||||
|
|
||||||
# an async generator instance
|
|
||||||
agen = await portal.run(
|
|
||||||
"piker.brokers.core", 'start_quote_stream',
|
|
||||||
broker=brokermod.name, tickers=tickers)
|
broker=brokermod.name, tickers=tickers)
|
||||||
|
|
||||||
async with trio.open_nursery() as nursery:
|
async with trio.open_nursery() as nursery:
|
||||||
# get first quotes response
|
# get first quotes response
|
||||||
log.debug("Waiting on first quote...")
|
log.debug("Waiting on first quote...")
|
||||||
quotes = await agen.__anext__()
|
quotes = await quote_gen.__anext__()
|
||||||
first_quotes = [
|
first_quotes = [
|
||||||
brokermod.format_quote(quote, symbol_data=sd)[0]
|
brokermod.format_quote(quote, symbol_data=sd)[0]
|
||||||
for quote in quotes.values()]
|
for quote in quotes.values()]
|
||||||
|
@ -413,7 +542,7 @@ async def _async_main(name, portal, tickers, brokermod, rate):
|
||||||
# build out UI
|
# build out UI
|
||||||
Window.set_title(f"monitor: {name}\t(press ? for help)")
|
Window.set_title(f"monitor: {name}\t(press ? for help)")
|
||||||
Builder.load_string(_kv)
|
Builder.load_string(_kv)
|
||||||
box = BoxLayout(orientation='vertical', padding=5, spacing=5)
|
box = BoxLayout(orientation='vertical', spacing=0)
|
||||||
|
|
||||||
# define bid-ask "stacked" cells
|
# define bid-ask "stacked" cells
|
||||||
# (TODO: needs some rethinking and renaming for sure)
|
# (TODO: needs some rethinking and renaming for sure)
|
||||||
|
@ -425,53 +554,53 @@ async def _async_main(name, portal, tickers, brokermod, rate):
|
||||||
{key: key for key in headers},
|
{key: key for key in headers},
|
||||||
headers=headers,
|
headers=headers,
|
||||||
bidasks=bidasks,
|
bidasks=bidasks,
|
||||||
is_header_row=True,
|
is_header=True,
|
||||||
size_hint=(1, None),
|
size_hint=(1, None),
|
||||||
)
|
)
|
||||||
box.add_widget(header)
|
box.add_widget(header)
|
||||||
|
|
||||||
# build grid
|
# build table
|
||||||
grid = TickerTable(
|
table = TickerTable(
|
||||||
cols=1,
|
cols=1,
|
||||||
size_hint=(1, None),
|
size_hint=(1, None),
|
||||||
)
|
)
|
||||||
for ticker_record in first_quotes:
|
for ticker_record in first_quotes:
|
||||||
grid.append_row(ticker_record, bidasks=bidasks)
|
table.append_row(ticker_record, bidasks=bidasks)
|
||||||
# associate the col headers row with the ticker table even though
|
# associate the col headers row with the ticker table even though
|
||||||
# they're technically wrapped separately in containing BoxLayout
|
# they're technically wrapped separately in containing BoxLayout
|
||||||
header.table = grid
|
header.table = table
|
||||||
|
|
||||||
# mark the initial sorted column header as bold and underlined
|
# mark the initial sorted column header as bold and underlined
|
||||||
sort_cell = header.get_cell(grid.sort_key)
|
sort_cell = header.get_cell(table.sort_key)
|
||||||
sort_cell.bold = sort_cell.underline = True
|
sort_cell.bold = sort_cell.underline = True
|
||||||
grid.last_clicked_col_cell = sort_cell
|
table.last_clicked_col_cell = sort_cell
|
||||||
|
|
||||||
# set up a pager view for large ticker lists
|
# set up a pager view for large ticker lists
|
||||||
grid.bind(minimum_height=grid.setter('height'))
|
table.bind(minimum_height=table.setter('height'))
|
||||||
pager = PagerView(box, grid, nursery)
|
pager = PagerView(box, table, nursery)
|
||||||
box.add_widget(pager)
|
box.add_widget(pager)
|
||||||
|
|
||||||
widgets = {
|
widgets = {
|
||||||
# 'anchor': anchor,
|
# 'anchor': anchor,
|
||||||
'root': box,
|
'root': box,
|
||||||
'grid': grid,
|
'table': table,
|
||||||
'box': box,
|
'box': box,
|
||||||
'header': header,
|
'header': header,
|
||||||
'pager': pager,
|
'pager': pager,
|
||||||
}
|
}
|
||||||
nursery.start_soon(
|
nursery.start_soon(
|
||||||
update_quotes, nursery, brokermod, widgets, agen, sd, quotes)
|
update_quotes, nursery, brokermod, widgets, quote_gen, sd, quotes)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Trio-kivy entry point.
|
# Trio-kivy entry point.
|
||||||
await async_runTouchApp(widgets['root']) # run kivy
|
await async_runTouchApp(widgets['root']) # run kivy
|
||||||
await agen.aclose() # cancel aysnc gen call
|
await quote_gen.aclose() # cancel aysnc gen call
|
||||||
finally:
|
finally:
|
||||||
# un-subscribe from symbols stream (cancel if brokerd
|
# un-subscribe from symbols stream (cancel if brokerd
|
||||||
# was already torn down - say by SIGINT)
|
# was already torn down - say by SIGINT)
|
||||||
with trio.move_on_after(0.2):
|
with trio.move_on_after(0.2):
|
||||||
await portal.run(
|
await portal.run(
|
||||||
"piker.brokers.core", 'modify_quote_stream',
|
"piker.brokers.data", 'modify_quote_stream',
|
||||||
broker=brokermod.name, tickers=[])
|
broker=brokermod.name, tickers=[])
|
||||||
|
|
||||||
# cancel GUI update task
|
# cancel GUI update task
|
||||||
|
|
|
@ -184,5 +184,5 @@ class PagerView(ScrollView):
|
||||||
_, yscale = self.convert_distance_to_scroll(0, pxs)
|
_, yscale = self.convert_distance_to_scroll(0, pxs)
|
||||||
new = self.scroll_y + (yscale * {'u': 1, 'd': -1}[direction])
|
new = self.scroll_y + (yscale * {'u': 1, 'd': -1}[direction])
|
||||||
# bound to near [0, 1] to avoid "over-scrolling"
|
# bound to near [0, 1] to avoid "over-scrolling"
|
||||||
limited = max(-0.03, min(new, 1.03))
|
limited = max(-0.01, min(new, 1.01))
|
||||||
self.scroll_y = limited
|
self.scroll_y = limited
|
||||||
|
|
Loading…
Reference in New Issue