Merge pull request #58 from pikers/monitor_polish

Monitor polish
kivy_mainline_and_py3.8
goodboy 2018-11-24 17:52:42 -05:00 committed by GitHub
commit 84f357b7eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 291 additions and 99 deletions

View File

@ -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``

View File

View File

@ -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)

View File

@ -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

View File

@ -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