From 274a5a728a1d749c06d01c9defd92ae0ee877188 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 12 Feb 2018 16:02:11 -0500 Subject: [PATCH 01/11] Fix row header borders using a `BorderImage` --- piker/ui/watchlist.py | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/piker/ui/watchlist.py b/piker/ui/watchlist.py index 357f4296..953ccd21 100644 --- a/piker/ui/watchlist.py +++ b/piker/ui/watchlist.py @@ -38,6 +38,9 @@ def colorcode(name): return _colors[name if name else 'gray'] +# border size +_bs = 5 + _kv = (f''' #:kivy 1.10.0 @@ -58,7 +61,7 @@ _kv = (f''' canvas.before: Color: rgb: [0.08]*4 - Rectangle: + BorderImage: pos: self.pos size: self.size @@ -68,17 +71,14 @@ _kv = (f''' background_color: [0]*4 canvas.before: Color: - rgb: [0.13]*4 - Rectangle: + rgb: [0.14]*4 + BorderImage: # use a fixed size border pos: self.pos - size: self.size - # RoundedRectangle: - # pos: self.pos - # size: self.size - # radius: [8,] + size: [self.size[0] - {_bs}, self.size[1]] + border: [0, {_bs} , 0, {_bs}] - spacing: '5dp' + spacing: '{_bs}dp' row_force_default: True row_default_height: 75 cols: 1 @@ -307,14 +307,13 @@ async def update_quotes( color_row(row, data) cache[sym] = (data, row) - # the core cell update loop + # core cell update loop while True: log.debug("Waiting on quotes") - quotes = await queue.get() + quotes = await queue.get() # new quotes data only for quote in quotes: data, displayable = qtconvert(quote, symbol_data=symbol_data) row = grid.symbols2rows[data['symbol']] - # only updates newly timestamped quotes cache[data['symbol']] = (data, row) # color changed field values @@ -346,10 +345,8 @@ async def update_quotes( async def run_kivy(root, nursery): '''Trio-kivy entry point. ''' - # run kivy - await async_runTouchApp(root) - # now cancel all the other tasks that may be running - nursery.cancel_scope.cancel() + await async_runTouchApp(root) # run kivy + nursery.cancel_scope.cancel() # cancel all other tasks that may be running async def _async_main(name, watchlists, brokermod): @@ -384,15 +381,12 @@ async def _async_main(name, watchlists, brokermod): header = header_row( first_quotes[0].keys(), size_hint=(1, None), - # put black lines between cells on the header row - spacing='3dp', ) root.add_widget(header) grid = ticker_table( first_quotes, size_hint=(1, None), ) - # associate the col headers row with the ticker table even though # they're technically wrapped separately in containing BoxLayout header.table = grid @@ -401,6 +395,7 @@ async def _async_main(name, watchlists, brokermod): sort_cell.bold = sort_cell.underline = True grid.last_clicked_col_cell = sort_cell + # set up a scroll view for large ticker lists grid.bind(minimum_height=grid.setter('height')) scroll = ScrollView() scroll.add_widget(grid) @@ -412,6 +407,5 @@ async def _async_main(name, watchlists, brokermod): 'header': header, 'scroll': scroll, } - nursery.start_soon(run_kivy, widgets['root'], nursery) nursery.start_soon(update_quotes, widgets, queue, sd, pkts) From 8e577cd0d006e072fbdc8f6b31513f707fcf01c1 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 12 Feb 2018 16:11:31 -0500 Subject: [PATCH 02/11] Use lighter red; sort rows on startup --- piker/ui/watchlist.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/piker/ui/watchlist.py b/piker/ui/watchlist.py index 953ccd21..ea9c053a 100644 --- a/piker/ui/watchlist.py +++ b/piker/ui/watchlist.py @@ -297,6 +297,16 @@ async def update_quotes( if hdrcell.background_color != [0]*4: hdrcell.background_color != color + def render_rows(pairs, sort_key='%'): + """Sort and render all rows on the ticker grid. + """ + grid.clear_widgets() + sort_key = grid.sort_key + for data, row in reversed( + sorted(pairs.values(), key=lambda item: item[0][sort_key]) + ): + grid.add_widget(row) # row append + cache = {} # initial coloring @@ -307,6 +317,8 @@ async def update_quotes( color_row(row, data) cache[sym] = (data, row) + render_rows(cache, grid.sort_key) + # core cell update loop while True: log.debug("Waiting on quotes") @@ -320,9 +332,9 @@ async def update_quotes( for key, val in data.items(): # logic for cell text coloring: up-green, down-red if row._last_record[key] < val: - color = colorcode('green') + color = colorcode('forestgreen') elif row._last_record[key] > val: - color = colorcode('red') + color = colorcode('red2') else: color = colorcode('gray') @@ -333,13 +345,7 @@ async def update_quotes( color_row(row, data) row._last_record = data - # sort rows by daily % change since open - grid.clear_widgets() - sort_key = grid.sort_key - for data, row in reversed( - sorted(cache.values(), key=lambda item: item[0][sort_key]) - ): - grid.add_widget(row) # row append + render_rows(cache, grid.sort_key) async def run_kivy(root, nursery): From e4ff113dfc0b378d99a4d3183246b1d5edcf2661 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 13 Feb 2018 10:34:24 -0500 Subject: [PATCH 03/11] Sort rows by column on click --- piker/ui/watchlist.py | 46 ++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/piker/ui/watchlist.py b/piker/ui/watchlist.py index ea9c053a..11c0c088 100644 --- a/piker/ui/watchlist.py +++ b/piker/ui/watchlist.py @@ -39,7 +39,8 @@ def colorcode(name): # border size -_bs = 5 +_bs = 3 +_color = [0.14]*4 # nice shade of gray _kv = (f''' #:kivy 1.10.0 @@ -60,21 +61,21 @@ _kv = (f''' outline_color: [0.1]*4 canvas.before: Color: - rgb: [0.08]*4 + rgb: {_color} BorderImage: pos: self.pos size: self.size - # bold: True font_size: '20' background_color: [0]*4 canvas.before: Color: - rgb: [0.14]*4 + rgb: {_color} BorderImage: # use a fixed size border pos: self.pos size: [self.size[0] - {_bs}, self.size[1]] + # 0s are because the containing TickerTable already has spacing border: [0, {_bs} , 0, {_bs}] @@ -162,10 +163,11 @@ class HeaderCell(Button): def on_press(self, value=None): # clicking on a col header indicates to rows by this column # in `update_quotes()` + table = self.row.table if self.row.is_header: - self.row.table.sort_key = self.key + table.sort_key = self.key - last = self.row.table.last_clicked_col_cell + last = table.last_clicked_col_cell if last and last is not self: last.underline = False last.bold = False @@ -174,7 +176,9 @@ class HeaderCell(Button): self.underline = True self.bold = True # mark this cell as the last - self.row.table.last_clicked_col_cell = self + table.last_clicked_col_cell = self + # sort and render the rows immediately + self.row.table.render_rows(table.quote_cache) # allow highlighting of row headers for tracking elif self.is_header: @@ -236,10 +240,11 @@ class Row(GridLayout): class TickerTable(GridLayout): """A grid for displaying ticker quote records as a table. """ - def __init__(self, sort_key='%', **kwargs): + def __init__(self, sort_key='%', quote_cache={}, **kwargs): super(TickerTable, self).__init__(**kwargs) self.symbols2rows = {} self.sort_key = sort_key + self.quote_cache = quote_cache # for tracking last clicked column header cell self.last_clicked_col_cell = None @@ -252,6 +257,16 @@ class TickerTable(GridLayout): self.add_widget(row) return row + def render_rows(self, pairs: (dict, Row), sort_key: str = None): + """Sort and render all rows on the ticker grid from ``pairs``. + """ + self.clear_widgets() + sort_key = sort_key or self.sort_key + for data, row in reversed( + sorted(pairs.values(), key=lambda item: item[0][sort_key]) + ): + self.add_widget(row) # row append + def header_row(headers, **kwargs): """Create a single "header" row from a sequence of keys. @@ -297,17 +312,8 @@ async def update_quotes( if hdrcell.background_color != [0]*4: hdrcell.background_color != color - def render_rows(pairs, sort_key='%'): - """Sort and render all rows on the ticker grid. - """ - grid.clear_widgets() - sort_key = grid.sort_key - for data, row in reversed( - sorted(pairs.values(), key=lambda item: item[0][sort_key]) - ): - grid.add_widget(row) # row append - cache = {} + grid.quote_cache = cache # initial coloring for quote in first_quotes: @@ -317,7 +323,7 @@ async def update_quotes( color_row(row, data) cache[sym] = (data, row) - render_rows(cache, grid.sort_key) + grid.render_rows(cache) # core cell update loop while True: @@ -345,7 +351,7 @@ async def update_quotes( color_row(row, data) row._last_record = data - render_rows(cache, grid.sort_key) + grid.render_rows(cache) async def run_kivy(root, nursery): From f31ebe6fcd0c4bcf9a19a8d2c59488007f72a546 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 13 Feb 2018 10:35:11 -0500 Subject: [PATCH 04/11] Handle numbers of magnitude 2 --- piker/calc.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/piker/calc.py b/piker/calc.py index 3d080087..0c891852 100644 --- a/piker/calc.py +++ b/piker/calc.py @@ -9,10 +9,12 @@ def humanize(number): """Convert large numbers to something with at most 3 digits and a letter suffix (eg. k: thousand, M: million, B: billion). """ - if number <= 0: + if not number or number <= 0: return number mag2suffix = {3: 'k', 6: 'M', 9: 'B'} mag = math.floor(math.log(number, 10)) + if mag < 3: + return number maxmag = max(itertools.takewhile(lambda key: mag >= key, mag2suffix)) return "{:.3f}{}".format(number/10**maxmag, mag2suffix[maxmag]) From 472ff688112c2099f046a0361cf4941eb4b9d1f7 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 13 Feb 2018 11:04:01 -0500 Subject: [PATCH 05/11] Fix row header highlight typo --- piker/ui/watchlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/piker/ui/watchlist.py b/piker/ui/watchlist.py index 11c0c088..76c7f1d7 100644 --- a/piker/ui/watchlist.py +++ b/piker/ui/watchlist.py @@ -310,7 +310,7 @@ async def update_quotes( # if the cell has been "highlighted" make sure to change its color if hdrcell.background_color != [0]*4: - hdrcell.background_color != color + hdrcell.background_color = color cache = {} grid.quote_cache = cache From 62bac7b2cdd235f3dd705043641b1c83d87b665c Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 13 Feb 2018 12:13:04 -0500 Subject: [PATCH 06/11] Don't sleep on less than zero delay --- piker/brokers/questrade.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/piker/brokers/questrade.py b/piker/brokers/questrade.py index bb5eb741..3f7cee8b 100644 --- a/piker/brokers/questrade.py +++ b/piker/brokers/questrade.py @@ -328,8 +328,9 @@ async def poll_tickers( last = _cache.setdefault(symbol, {}) timekey = 'lastTradeTime' if quote[timekey] != last.get(timekey): + new = set(quote.items()) - set(last.items()) log.info( - f"New quote {quote['symbol']} @ {quote[timekey]}") + f"New quote {quote['symbol']} @ {quote[timekey]}:\n{new}") _cache[symbol] = quote payload.append(quote) else: @@ -342,7 +343,8 @@ async def poll_tickers( delay = sleeptime - proc_time if delay <= 0: log.warn(f"Took {proc_time} seconds for processing quotes?") - await trio.sleep(delay) + else: + await trio.sleep(delay) async def api(methname: str, **kwargs) -> dict: From 29be7f58c99bbcbb9dedf24ed0badb74c5dd39fe Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 13 Feb 2018 15:35:21 -0500 Subject: [PATCH 07/11] Push every new change; not just the last trade --- piker/brokers/questrade.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/piker/brokers/questrade.py b/piker/brokers/questrade.py index 3f7cee8b..caaa9c65 100644 --- a/piker/brokers/questrade.py +++ b/piker/brokers/questrade.py @@ -301,7 +301,7 @@ async def poll_tickers( client: Client, tickers: [str], q: trio.Queue, rate: int = 5, # 200ms delay between quotes - time_cached: bool = True, # only deliver "new" quotes to the queue + diff_cached: bool = True, # only deliver "new" quotes to the queue ) -> None: """Stream quotes for a sequence of tickers at the given ``rate`` per second. @@ -323,14 +323,14 @@ async def poll_tickers( if quote['delay'] > 0: log.warning(f"Delayed quote:\n{quote}") - if time_cached: # if cache is enabled then only deliver "new" changes + if diff_cached: + # if cache is enabled then only deliver "new" changes symbol = quote['symbol'] last = _cache.setdefault(symbol, {}) - timekey = 'lastTradeTime' - if quote[timekey] != last.get(timekey): - new = set(quote.items()) - set(last.items()) + new = set(quote.items()) - set(last.items()) + if new: log.info( - f"New quote {quote['symbol']} @ {quote[timekey]}:\n{new}") + f"New quote {quote['symbol']}:\n{new}") _cache[symbol] = quote payload.append(quote) else: From e464898210007a2027daf68fee96efe4e26d4843 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Wed, 14 Feb 2018 02:07:42 -0500 Subject: [PATCH 08/11] Combine last,bid,ask in a StackLayout-cell --- piker/ui/watchlist.py | 157 +++++++++++++++++++++++++++++++----------- 1 file changed, 115 insertions(+), 42 deletions(-) diff --git a/piker/ui/watchlist.py b/piker/ui/watchlist.py index 76c7f1d7..1d3fa384 100644 --- a/piker/ui/watchlist.py +++ b/piker/ui/watchlist.py @@ -5,11 +5,14 @@ Launch with ``piker watch ``. (Currently there's a bunch of QT specific stuff in here) """ +from itertools import chain +from functools import partial + import trio from kivy.uix.boxlayout import BoxLayout from kivy.uix.gridlayout import GridLayout +from kivy.uix.stacklayout import StackLayout from kivy.uix.button import Button -from kivy.uix.label import Label from kivy.uix.scrollview import ScrollView from kivy.lang import Builder from kivy import utils @@ -40,34 +43,26 @@ def colorcode(name): # border size _bs = 3 -_color = [0.14]*4 # nice shade of gray +_color = [0.13]*3 # nice shade of gray _kv = (f''' #:kivy 1.10.0 + font_size: 20 text_size: self.size size: self.texture_size - font_size: '20' color: {colorcode('gray')} - # size_hint_y: None font_color: {colorcode('gray')} font_name: 'Roboto-Regular' - # height: 50 - # width: 50 - background_color: [0]*4 + background_color: [0.13]*3 + [1] + background_normal: '' valign: 'middle' halign: 'center' outline_color: [0.1]*4 - canvas.before: - Color: - rgb: {_color} - BorderImage: - pos: self.pos - size: self.size - font_size: '20' + font_size: 21 background_color: [0]*4 canvas.before: Color: @@ -84,11 +79,14 @@ _kv = (f''' row_default_height: 75 cols: 1 + + spacing: [{_bs}, 0] + - minimum_height: 200 # should be pulled from Cell text size - minimum_width: 200 - row_force_default: True - row_default_height: 75 + # minimum_height: 200 # should be pulled from Cell text size + # minimum_width: 200 + # row_force_default: True + # row_default_height: 75 outline_color: [.7]*4 ''') @@ -99,19 +97,19 @@ _qt_keys = { 'lastTradePrice': 'last', 'askPrice': 'ask', 'bidPrice': 'bid', - 'lastTradeSize': 'last size', - 'bidSize': 'bid size', - 'askSize': 'ask size', + 'lastTradeSize': 'size', + 'bidSize': 'bsize', + 'askSize': 'asize', 'volume': ('vol', humanize), - 'VWAP': ('VWAP', "{:.3f}".format), - 'high52w': 'high52w', + 'VWAP': ('VWAP', partial(round, ndigits=3)), + 'openPrice': 'open', + 'lowPrice': 'low', 'highPrice': 'high', + 'low52w': 'low52w', + 'high52w': 'high52w', # "lastTradePriceTrHrs": 7.99, # "lastTradeTick": "Equal", # "lastTradeTime": "2018-01-30T18:28:23.434000-05:00", - # 'low52w': 'low52w', - 'lowPrice': 'low day', - 'openPrice': 'open', # "symbolId": 3575753, # "tier": "", # 'isHalted': 'halted', @@ -188,11 +186,62 @@ class HeaderCell(Button): self.background_color = self.color -class Cell(Label): - """Data cell label. +class Cell(Button): + """Data cell. """ +class BidAskLayout(StackLayout): + """Cell which houses three buttons containing a last, bid, and ask in a + single unit oriented with the last 2 under the first. + """ + def __init__(self, values, header=False, **kwargs): + super(BidAskLayout, self).__init__(orientation='lr-tb', **kwargs) + assert len(values) == 3, "You can only provide 3 values: last,bid,ask" + self._keys2cells = {} + cell_type = HeaderCell if header else Cell + top_size = cell_type().font_size + small_size = top_size - 5 + top_prop = 0.7 + bottom_prop = 1 - top_prop + for (key, size_hint, font_size), value in zip( + [('last', (1, top_prop), top_size), + ('bid', (0.5, bottom_prop), small_size), + ('ask', (0.5, bottom_prop), small_size)], + values + ): + cell = cell_type( + text=str(value), + size_hint=size_hint, + width=self.width/2 - 3, + font_size=font_size + ) + self._keys2cells[key] = cell + cell.key = value + cell.is_header = header + setattr(self, key, cell) + self.add_widget(cell) + + # should be assigned by referrer + self.row = None + + def get_cell(self, key): + return self._keys2cells[key] + + @property + def row(self): + return self.row + + @row.setter + def row(self, row): + for cell in self.cells: + cell.row = row + + @property + def cells(self): + return [self.last, self.bid, self.ask] + + class Row(GridLayout): """A grid for displaying a row of ticker quote data. @@ -209,12 +258,41 @@ class Row(GridLayout): self.table = table self.is_header = is_header_row + # create `BidAskCells` first + bidasks = { + 'last': ['bid', 'ask'], + 'size': ['bsize', 'asize'] + } + # import pdb; pdb.set_trace() + ba_cells = {} + layouts = {} + for key, children in bidasks.items(): + layout = BidAskLayout( + [record[key]] + [record[child] for child in children], + header=is_header_row + ) + layout.row = self + layouts[key] = layout + for i, child in enumerate([key] + children): + ba_cells[child] = layout.cells[i] + + children_flat = list(chain.from_iterable(bidasks.values())) + self._cell_widgets.update(ba_cells) + # build out row using Cell labels for (key, val) in record.items(): header = key in headers - cell = self._append_cell(val, header=header) - self._cell_widgets[key] = cell - cell.key = key + + # handle bidask cells + if key in layouts: + self.add_widget(layouts[key]) + elif key in children_flat: + # these cells have already been added to the `BidAskLayout` + continue + else: + cell = self._append_cell(val, header=header) + cell.key = key + self._cell_widgets[key] = cell def get_cell(self, key): return self._cell_widgets[key] @@ -228,11 +306,6 @@ class Row(GridLayout): cell = celltype(text=str(text)) cell.is_header = header cell.row = self - - # don't bold the header row - if header and self.is_header: - cell.bold = False - self.add_widget(cell) return cell @@ -296,8 +369,8 @@ async def update_quotes( grid = widgets['grid'] def color_row(row, data): - hdrcell = row._cell_widgets['symbol'] - chngcell = row._cell_widgets['%'] + hdrcell = row.get_cell('symbol') + chngcell = row.get_cell('%') daychange = float(data['%']) if daychange < 0.: color = colorcode('red2') @@ -319,7 +392,7 @@ async def update_quotes( for quote in first_quotes: sym = quote['symbol'] row = grid.symbols2rows[sym] - data, _ = qtconvert(quote, symbol_data=symbol_data) + data, displayable = qtconvert(quote, symbol_data=symbol_data) color_row(row, data) cache[sym] = (data, row) @@ -344,7 +417,7 @@ async def update_quotes( else: color = colorcode('gray') - cell = row._cell_widgets[key] + cell = row.get_cell(key) cell.text = str(displayable[key]) cell.color = color @@ -385,11 +458,11 @@ async def _async_main(name, watchlists, brokermod): return first_quotes = [ - qtconvert(quote, symbol_data=sd)[0] for quote in pkts] + qtconvert(quote, symbol_data=sd)[1] for quote in pkts] # build out UI Builder.load_string(_kv) - root = BoxLayout(orientation='vertical', padding=5, spacing=-20) + root = BoxLayout(orientation='vertical', padding=5, spacing=5) header = header_row( first_quotes[0].keys(), size_hint=(1, None), From 722b515246e1a95fad9b6992fd019045761f64e5 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Wed, 14 Feb 2018 02:43:55 -0500 Subject: [PATCH 09/11] Limit humanize output to 2 decimal places --- piker/calc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/piker/calc.py b/piker/calc.py index 0c891852..f5efae25 100644 --- a/piker/calc.py +++ b/piker/calc.py @@ -16,7 +16,7 @@ def humanize(number): if mag < 3: return number maxmag = max(itertools.takewhile(lambda key: mag >= key, mag2suffix)) - return "{:.3f}{}".format(number/10**maxmag, mag2suffix[maxmag]) + return "{:.2f}{}".format(number/10**maxmag, mag2suffix[maxmag]) def percent_change(init, new): From 722d29491544a45a829d296a8ee7aa5049af492e Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Wed, 14 Feb 2018 12:06:29 -0500 Subject: [PATCH 10/11] Handle non-numbers in `humanize()` --- piker/calc.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/piker/calc.py b/piker/calc.py index f5efae25..0ed811fc 100644 --- a/piker/calc.py +++ b/piker/calc.py @@ -9,6 +9,10 @@ def humanize(number): """Convert large numbers to something with at most 3 digits and a letter suffix (eg. k: thousand, M: million, B: billion). """ + try: + float(number) + except ValueError: + return 0 if not number or number <= 0: return number mag2suffix = {3: 'k', 6: 'M', 9: 'B'} From d50aa17a83c2ed6dbee3c2fe9201eda1ed9918d5 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Wed, 14 Feb 2018 12:06:54 -0500 Subject: [PATCH 11/11] Add real-time market caps --- piker/ui/watchlist.py | 86 +++++++++++++++++++++++-------------------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/piker/ui/watchlist.py b/piker/ui/watchlist.py index 1d3fa384..f2d8a8ff 100644 --- a/piker/ui/watchlist.py +++ b/piker/ui/watchlist.py @@ -41,15 +41,13 @@ def colorcode(name): return _colors[name if name else 'gray'] -# border size -_bs = 3 +_bs = 3 # border size _color = [0.13]*3 # nice shade of gray - _kv = (f''' #:kivy 1.10.0 - font_size: 20 + font_size: 18 text_size: self.size size: self.texture_size color: {colorcode('gray')} @@ -62,7 +60,7 @@ _kv = (f''' outline_color: [0.1]*4 - font_size: 21 + font_size: 20 background_color: [0]*4 canvas.before: Color: @@ -91,17 +89,19 @@ _kv = (f''' ''') -# Questrade key conversion +# Questrade key conversion / column order _qt_keys = { - # 'symbol': 'symbol', # done manually in qtconvert + 'symbol': 'symbol', # done manually in qtconvert + '%': '%', 'lastTradePrice': 'last', 'askPrice': 'ask', 'bidPrice': 'bid', 'lastTradeSize': 'size', 'bidSize': 'bsize', 'askSize': 'asize', - 'volume': ('vol', humanize), 'VWAP': ('VWAP', partial(round, ndigits=3)), + 'volume': ('vol', humanize), + 'mktcap': ('mktcap', humanize), 'openPrice': 'open', 'lowPrice': 'low', 'highPrice': 'high', @@ -127,19 +127,25 @@ def qtconvert( and the second is the same but with all values converted to a "display-friendly" string format. """ + last = quote['lastTradePrice'] + symbol = quote['symbol'] if symbol_data: # we can only compute % change from symbols data - previous = symbol_data[quote['symbol']]['prevDayClosePrice'] - change = percent_change(previous, quote['lastTradePrice']) + previous = symbol_data[symbol]['prevDayClosePrice'] + change = percent_change(previous, last) + share_count = symbol_data[symbol].get('outstandingShares', None) + mktcap = share_count * last if share_count else 'NA' else: change = 0 - new = { + computed = { 'symbol': quote['symbol'], - '%': round(change, 3) + '%': round(change, 3), + 'mktcap': mktcap, } - displayable = new.copy() + new = {} + displayable = {} for key, new_key in keymap.items(): - display_value = value = quote[key] + display_value = value = quote.get(key) or computed.get(key) # API servers can return `None` vals when markets are closed (weekend) value = 0 if value is None else value @@ -263,7 +269,6 @@ class Row(GridLayout): 'last': ['bid', 'ask'], 'size': ['bsize', 'asize'] } - # import pdb; pdb.set_trace() ba_cells = {} layouts = {} for key, children in bidasks.items(): @@ -309,6 +314,23 @@ class Row(GridLayout): self.add_widget(cell) return cell + def update(self, record, displayable): + # color changed field values + for key, val in record.items(): + # logic for cell text coloring: up-green, down-red + if self._last_record[key] < val: + color = colorcode('forestgreen') + elif self._last_record[key] > val: + color = colorcode('red2') + else: + color = colorcode('gray') + + cell = self.get_cell(key) + cell.text = str(displayable[key]) + cell.color = color + + self._last_record = record + class TickerTable(GridLayout): """A grid for displaying ticker quote records as a table. @@ -392,9 +414,10 @@ async def update_quotes( for quote in first_quotes: sym = quote['symbol'] row = grid.symbols2rows[sym] - data, displayable = qtconvert(quote, symbol_data=symbol_data) - color_row(row, data) - cache[sym] = (data, row) + record, displayable = qtconvert(quote, symbol_data=symbol_data) + row.update(record, displayable) + color_row(row, record) + cache[sym] = (record, row) grid.render_rows(cache) @@ -403,26 +426,11 @@ async def update_quotes( log.debug("Waiting on quotes") quotes = await queue.get() # new quotes data only for quote in quotes: - data, displayable = qtconvert(quote, symbol_data=symbol_data) - row = grid.symbols2rows[data['symbol']] - cache[data['symbol']] = (data, row) - - # color changed field values - for key, val in data.items(): - # logic for cell text coloring: up-green, down-red - if row._last_record[key] < val: - color = colorcode('forestgreen') - elif row._last_record[key] > val: - color = colorcode('red2') - else: - color = colorcode('gray') - - cell = row.get_cell(key) - cell.text = str(displayable[key]) - cell.color = color - - color_row(row, data) - row._last_record = data + record, displayable = qtconvert(quote, symbol_data=symbol_data) + row = grid.symbols2rows[record['symbol']] + cache[record['symbol']] = (record, row) + row.update(record, displayable) + color_row(row, record) grid.render_rows(cache) @@ -458,7 +466,7 @@ async def _async_main(name, watchlists, brokermod): return first_quotes = [ - qtconvert(quote, symbol_data=sd)[1] for quote in pkts] + qtconvert(quote, symbol_data=sd)[0] for quote in pkts] # build out UI Builder.load_string(_kv)