commit
d2e1605bf0
|
@ -122,7 +122,13 @@ def watch(loglevel, broker, watchlist_name):
|
||||||
'APH.TO', 'MARI.CN', 'WMD.VN', 'LEAF.TO', 'THCX.VN',
|
'APH.TO', 'MARI.CN', 'WMD.VN', 'LEAF.TO', 'THCX.VN',
|
||||||
'WEED.TO', 'NINE.VN', 'RTI.VN', 'SNN.CN', 'ACB.TO',
|
'WEED.TO', 'NINE.VN', 'RTI.VN', 'SNN.CN', 'ACB.TO',
|
||||||
'OGI.VN', 'IMH.VN', 'FIRE.VN', 'EAT.CN', 'NUU.VN',
|
'OGI.VN', 'IMH.VN', 'FIRE.VN', 'EAT.CN', 'NUU.VN',
|
||||||
'WMD.VN', 'HEMP.VN', 'CALI.CN', 'RBQ.CN',
|
'WMD.VN', 'HEMP.VN', 'CALI.CN', 'RQB.CN', 'MPX.CN',
|
||||||
|
'SEED.TO', 'HMJR.TO', 'CMED.TO', 'PAS.VN',
|
||||||
|
'CRON',
|
||||||
|
],
|
||||||
|
'dad': [
|
||||||
|
'GM', 'TSLA', 'DOL.TO', 'CIM', 'SPY',
|
||||||
|
'SHOP.TO',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
# broker_conf_path = os.path.join(
|
# broker_conf_path = os.path.join(
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
Keyboard controls
|
Pager widget + kb controls
|
||||||
"""
|
"""
|
||||||
import inspect
|
import inspect
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
@ -75,30 +75,74 @@ async def handle_input(widget, patts2funcs: dict, patt_len_limit=3):
|
||||||
|
|
||||||
|
|
||||||
class SearchBar(TextInput):
|
class SearchBar(TextInput):
|
||||||
def __init__(self, kbctls: dict, parent, **kwargs):
|
def __init__(self, kbctls: dict, container: 'Widget', pager: 'PagerView',
|
||||||
|
searcher, **kwargs):
|
||||||
super(SearchBar, self).__init__(
|
super(SearchBar, self).__init__(
|
||||||
multiline=False,
|
multiline=False,
|
||||||
# text='/',
|
hint_text='Ticker Search',
|
||||||
|
cursor_blink=False,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
self.foreground_color = self.hint_text_color # actually readable
|
||||||
self.kbctls = kbctls
|
self.kbctls = kbctls
|
||||||
self._container = parent
|
self._container = container
|
||||||
|
self._pager = pager
|
||||||
|
self._searcher = searcher
|
||||||
# indicate to ``handle_input`` that search is activated on '/'
|
# indicate to ``handle_input`` that search is activated on '/'
|
||||||
self.kbctls.update({
|
self.kbctls.update({
|
||||||
('/',): self.handle_input
|
('/',): self.handle_input
|
||||||
})
|
})
|
||||||
|
self._sugg_template = ' '*4 + '{} matches: '
|
||||||
|
self._matched = []
|
||||||
|
|
||||||
|
def suggest(self, matches):
|
||||||
|
self.suggestion_text = ''
|
||||||
|
suffix = self._sugg_template.format(len(matches) or "No")
|
||||||
|
self.suggestion_text = suffix + ' '.join(matches)
|
||||||
|
|
||||||
def on_text(self, instance, value):
|
def on_text(self, instance, value):
|
||||||
# ``scroll.scroll_to()`` looks super awesome here for when we
|
|
||||||
# our interproc-ticker protocol
|
def unhighlight(cells):
|
||||||
# async for item in self.async_bind('text'):
|
for cell in cells:
|
||||||
print(value)
|
cell.outline_color = [0]*3
|
||||||
|
cell.outline_width = 0
|
||||||
|
self._matched.remove(cell)
|
||||||
|
|
||||||
|
if not value:
|
||||||
|
if self._matched:
|
||||||
|
unhighlight(self._matched.copy())
|
||||||
|
return
|
||||||
|
|
||||||
|
if not value.isupper():
|
||||||
|
self.text = value.upper()
|
||||||
|
return
|
||||||
|
|
||||||
|
text = self.text
|
||||||
|
matches = self._searcher.search(text)
|
||||||
|
self.suggest(matches.keys())
|
||||||
|
|
||||||
|
if matches:
|
||||||
|
# unhighlight old matches
|
||||||
|
unmatched = set(self._matched) - set(matches.keys())
|
||||||
|
unhighlight(unmatched)
|
||||||
|
|
||||||
|
for key, widget in matches.items():
|
||||||
|
cell = widget.get_cell('symbol')
|
||||||
|
# cell.background_color = [0.4]*3 + [1]
|
||||||
|
cell.outline_width = 2
|
||||||
|
cell.outline_color = [0.6] * 3
|
||||||
|
self._matched.append(cell)
|
||||||
|
|
||||||
|
key, widget = list(matches.items())[0]
|
||||||
|
# ensure first match widget is displayed
|
||||||
|
self._pager.scroll_to(widget)
|
||||||
|
|
||||||
async def handle_input(self):
|
async def handle_input(self):
|
||||||
self._container.add_widget(self) # display it
|
self._container.add_widget(self) # display it
|
||||||
self.focus = True # focus immediately (doesn't work from __init__)
|
self.focus = True # focus immediately (doesn't work from __init__)
|
||||||
# wait for <enter> to close search bar
|
# wait for <enter> to close search bar
|
||||||
await self.async_bind('on_text_validate').__aiter__().__anext__()
|
await self.async_bind('on_text_validate').__aiter__().__anext__()
|
||||||
|
log.debug(f"Seach text is {self.text}")
|
||||||
|
|
||||||
log.debug("Closing search bar")
|
log.debug("Closing search bar")
|
||||||
self._container.remove_widget(self) # stop displaying
|
self._container.remove_widget(self) # stop displaying
|
||||||
|
@ -108,7 +152,8 @@ class SearchBar(TextInput):
|
||||||
class PagerView(ScrollView):
|
class PagerView(ScrollView):
|
||||||
"""Pager view that adds less-like scrolling and search.
|
"""Pager view that adds less-like scrolling and search.
|
||||||
"""
|
"""
|
||||||
def __init__(self, container, nursery, kbctls: dict = None, **kwargs):
|
def __init__(self, container, contained, nursery, kbctls: dict = None,
|
||||||
|
**kwargs):
|
||||||
super(PagerView, self).__init__(**kwargs)
|
super(PagerView, self).__init__(**kwargs)
|
||||||
self._container = container
|
self._container = container
|
||||||
self.kbctls = kbctls or {}
|
self.kbctls = kbctls or {}
|
||||||
|
@ -119,7 +164,11 @@ class PagerView(ScrollView):
|
||||||
('d',): partial(self.halfpage_y, 'd'),
|
('d',): partial(self.halfpage_y, 'd'),
|
||||||
# ('?',):
|
# ('?',):
|
||||||
})
|
})
|
||||||
self.search = SearchBar(self.kbctls, container)
|
# add contained child widget (can only be one)
|
||||||
|
self._contained = contained
|
||||||
|
self.add_widget(contained)
|
||||||
|
self.search = SearchBar(
|
||||||
|
self.kbctls, container, self, searcher=contained)
|
||||||
# spawn kb handler task
|
# spawn kb handler task
|
||||||
nursery.start_soon(handle_input, self, self.kbctls)
|
nursery.start_soon(handle_input, self, self.kbctls)
|
||||||
|
|
|
@ -21,7 +21,7 @@ from kivy.core.window import Window
|
||||||
|
|
||||||
from ..calc import humanize, percent_change
|
from ..calc import humanize, percent_change
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from .kb import PagerView
|
from .pager import PagerView
|
||||||
|
|
||||||
log = get_logger('watchlist')
|
log = get_logger('watchlist')
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ _kv = (f'''
|
||||||
|
|
||||||
<Cell>
|
<Cell>
|
||||||
font_size: 18
|
font_size: 18
|
||||||
text_size: self.size
|
# text_size: self.size
|
||||||
size: self.texture_size
|
size: self.texture_size
|
||||||
color: {colorcode('gray')}
|
color: {colorcode('gray')}
|
||||||
font_color: {colorcode('gray')}
|
font_color: {colorcode('gray')}
|
||||||
|
@ -59,7 +59,7 @@ _kv = (f'''
|
||||||
background_normal: ''
|
background_normal: ''
|
||||||
valign: 'middle'
|
valign: 'middle'
|
||||||
halign: 'center'
|
halign: 'center'
|
||||||
outline_color: [0.1]*4
|
# outline_color: [0.1]*4
|
||||||
|
|
||||||
<HeaderCell>
|
<HeaderCell>
|
||||||
font_size: 20
|
font_size: 20
|
||||||
|
@ -87,9 +87,10 @@ _kv = (f'''
|
||||||
# minimum_width: 200
|
# minimum_width: 200
|
||||||
# row_force_default: True
|
# row_force_default: True
|
||||||
# row_default_height: 75
|
# row_default_height: 75
|
||||||
outline_color: [.7]*4
|
# outline_color: [.7]*4
|
||||||
|
|
||||||
<SearchBar>
|
<SearchBar>
|
||||||
|
# part of the `PagerView`
|
||||||
size_hint: 1, 0.03
|
size_hint: 1, 0.03
|
||||||
font_size: 25
|
font_size: 25
|
||||||
background_color: [0.13]*3 + [1]
|
background_color: [0.13]*3 + [1]
|
||||||
|
@ -125,10 +126,12 @@ _qt_keys = {
|
||||||
|
|
||||||
|
|
||||||
def qtconvert(
|
def qtconvert(
|
||||||
quote: dict, keymap: dict = _qt_keys, symbol_data: dict = None
|
quote: dict, symbol_data: dict,
|
||||||
|
keymap: dict = _qt_keys,
|
||||||
) -> (dict, dict):
|
) -> (dict, dict):
|
||||||
"""Remap a list of quote dicts ``quotes`` using the mapping of old keys
|
"""Remap a list of quote dicts ``quotes`` using the mapping of old keys
|
||||||
-> new keys ``keymap``.
|
-> new keys ``keymap`` returning 2 dicts: one with raw data and the other
|
||||||
|
for display.
|
||||||
|
|
||||||
Returns 2 dicts: first is the original values mapped by new keys,
|
Returns 2 dicts: first is the original values mapped by new keys,
|
||||||
and the second is the same but with all values converted to a
|
and the second is the same but with all values converted to a
|
||||||
|
@ -136,13 +139,10 @@ def qtconvert(
|
||||||
"""
|
"""
|
||||||
last = quote['lastTradePrice']
|
last = quote['lastTradePrice']
|
||||||
symbol = quote['symbol']
|
symbol = quote['symbol']
|
||||||
if symbol_data: # we can only compute % change from symbols data
|
previous = symbol_data[symbol]['prevDayClosePrice']
|
||||||
previous = symbol_data[symbol]['prevDayClosePrice']
|
change = percent_change(previous, last)
|
||||||
change = percent_change(previous, last)
|
share_count = symbol_data[symbol].get('outstandingShares', None)
|
||||||
share_count = symbol_data[symbol].get('outstandingShares', None)
|
mktcap = share_count * last if share_count else 'NA'
|
||||||
mktcap = share_count * last if share_count else 'NA'
|
|
||||||
else:
|
|
||||||
change = 0
|
|
||||||
computed = {
|
computed = {
|
||||||
'symbol': quote['symbol'],
|
'symbol': quote['symbol'],
|
||||||
'%': round(change, 3),
|
'%': round(change, 3),
|
||||||
|
@ -157,7 +157,7 @@ def qtconvert(
|
||||||
# API servers can return `None` vals when markets are closed (weekend)
|
# API servers can return `None` vals when markets are closed (weekend)
|
||||||
value = 0 if value is None else value
|
value = 0 if value is None else value
|
||||||
|
|
||||||
# convert values to a displayble format
|
# convert values to a displayble format using available formatting func
|
||||||
if isinstance(new_key, tuple):
|
if isinstance(new_key, tuple):
|
||||||
new_key, func = new_key
|
new_key, func = new_key
|
||||||
display_value = func(value)
|
display_value = func(value)
|
||||||
|
@ -172,8 +172,9 @@ class HeaderCell(Button):
|
||||||
"""Column header cell label.
|
"""Column header cell label.
|
||||||
"""
|
"""
|
||||||
def on_press(self, value=None):
|
def on_press(self, value=None):
|
||||||
# clicking on a col header indicates to rows by this column
|
"""Clicking on a col header indicates to sort rows by this column
|
||||||
# in `update_quotes()`
|
in `update_quotes()`.
|
||||||
|
"""
|
||||||
table = self.row.table
|
table = self.row.table
|
||||||
if self.row.is_header:
|
if self.row.is_header:
|
||||||
table.sort_key = self.key
|
table.sort_key = self.key
|
||||||
|
@ -215,7 +216,7 @@ class BidAskLayout(StackLayout):
|
||||||
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 - 5
|
small_size = top_size - 5
|
||||||
top_prop = 0.7
|
top_prop = 0.7 # 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),
|
||||||
|
@ -375,22 +376,18 @@ class TickerTable(GridLayout):
|
||||||
):
|
):
|
||||||
self.add_widget(row) # row append
|
self.add_widget(row) # row append
|
||||||
|
|
||||||
|
def ticker_search(self, patt):
|
||||||
|
"""Return sequence of matches when pattern ``patt`` is in a
|
||||||
|
symbol name. Most naive algo possible for the moment.
|
||||||
|
"""
|
||||||
|
for symbol, row in self.symbols2rows.items():
|
||||||
|
if patt in symbol:
|
||||||
|
yield symbol, row
|
||||||
|
|
||||||
def header_row(headers, **kwargs):
|
def search(self, patt):
|
||||||
"""Create a single "header" row from a sequence of keys.
|
"""Search bar api compat.
|
||||||
"""
|
"""
|
||||||
headers_dict = {key: key for key in headers}
|
return dict(self.ticker_search(patt)) or {}
|
||||||
row = Row(headers_dict, headers=headers, is_header_row=True, **kwargs)
|
|
||||||
return row
|
|
||||||
|
|
||||||
|
|
||||||
def ticker_table(quotes, **kwargs):
|
|
||||||
"""Create a new ticker table from a list of quote dicts.
|
|
||||||
"""
|
|
||||||
table = TickerTable(cols=1, **kwargs)
|
|
||||||
for ticker_record in quotes:
|
|
||||||
table.append_row(ticker_record)
|
|
||||||
return table
|
|
||||||
|
|
||||||
|
|
||||||
async def update_quotes(
|
async def update_quotes(
|
||||||
|
@ -484,21 +481,29 @@ async def _async_main(name, watchlists, brokermod):
|
||||||
# build out UI
|
# build out UI
|
||||||
Window.set_title(f"watchlist: {name}\t(press ? for help)")
|
Window.set_title(f"watchlist: {name}\t(press ? for help)")
|
||||||
Builder.load_string(_kv)
|
Builder.load_string(_kv)
|
||||||
# anchor = AnchorLayout(anchor_x='right', anchor_y='bottom')
|
|
||||||
box = BoxLayout(orientation='vertical', padding=5, spacing=5)
|
box = BoxLayout(orientation='vertical', padding=5, spacing=5)
|
||||||
# anchor.add_widget(box)
|
|
||||||
header = header_row(
|
# add header row
|
||||||
first_quotes[0].keys(),
|
headers = first_quotes[0].keys()
|
||||||
|
header = Row(
|
||||||
|
{key: key for key in headers},
|
||||||
|
headers=headers,
|
||||||
|
is_header_row=True,
|
||||||
size_hint=(1, None),
|
size_hint=(1, None),
|
||||||
)
|
)
|
||||||
box.add_widget(header)
|
box.add_widget(header)
|
||||||
grid = ticker_table(
|
|
||||||
first_quotes,
|
# build grid
|
||||||
|
grid = TickerTable(
|
||||||
|
cols=1,
|
||||||
size_hint=(1, None),
|
size_hint=(1, None),
|
||||||
)
|
)
|
||||||
|
for ticker_record in first_quotes:
|
||||||
|
grid.append_row(ticker_record)
|
||||||
# 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 = grid
|
||||||
|
|
||||||
# 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(grid.sort_key)
|
||||||
sort_cell.bold = sort_cell.underline = True
|
sort_cell.bold = sort_cell.underline = True
|
||||||
|
@ -506,8 +511,7 @@ async def _async_main(name, watchlists, brokermod):
|
||||||
|
|
||||||
# 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'))
|
grid.bind(minimum_height=grid.setter('height'))
|
||||||
pager = PagerView(box, nursery)
|
pager = PagerView(box, grid, nursery)
|
||||||
pager.add_widget(grid)
|
|
||||||
box.add_widget(pager)
|
box.add_widget(pager)
|
||||||
|
|
||||||
widgets = {
|
widgets = {
|
||||||
|
|
Loading…
Reference in New Issue