commit
0faec1537e
|
@ -301,7 +301,7 @@ async def poll_tickers(
|
||||||
client: Client, tickers: [str],
|
client: Client, tickers: [str],
|
||||||
q: trio.Queue,
|
q: trio.Queue,
|
||||||
rate: int = 5, # 200ms delay between quotes
|
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:
|
) -> None:
|
||||||
"""Stream quotes for a sequence of tickers at the given ``rate``
|
"""Stream quotes for a sequence of tickers at the given ``rate``
|
||||||
per second.
|
per second.
|
||||||
|
@ -323,13 +323,14 @@ async def poll_tickers(
|
||||||
if quote['delay'] > 0:
|
if quote['delay'] > 0:
|
||||||
log.warning(f"Delayed quote:\n{quote}")
|
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']
|
symbol = quote['symbol']
|
||||||
last = _cache.setdefault(symbol, {})
|
last = _cache.setdefault(symbol, {})
|
||||||
timekey = 'lastTradeTime'
|
new = set(quote.items()) - set(last.items())
|
||||||
if quote[timekey] != last.get(timekey):
|
if new:
|
||||||
log.info(
|
log.info(
|
||||||
f"New quote {quote['symbol']} @ {quote[timekey]}")
|
f"New quote {quote['symbol']}:\n{new}")
|
||||||
_cache[symbol] = quote
|
_cache[symbol] = quote
|
||||||
payload.append(quote)
|
payload.append(quote)
|
||||||
else:
|
else:
|
||||||
|
@ -342,7 +343,8 @@ async def poll_tickers(
|
||||||
delay = sleeptime - proc_time
|
delay = sleeptime - proc_time
|
||||||
if delay <= 0:
|
if delay <= 0:
|
||||||
log.warn(f"Took {proc_time} seconds for processing quotes?")
|
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:
|
async def api(methname: str, **kwargs) -> dict:
|
||||||
|
|
|
@ -9,12 +9,18 @@ def humanize(number):
|
||||||
"""Convert large numbers to something with at most 3 digits and
|
"""Convert large numbers to something with at most 3 digits and
|
||||||
a letter suffix (eg. k: thousand, M: million, B: billion).
|
a letter suffix (eg. k: thousand, M: million, B: billion).
|
||||||
"""
|
"""
|
||||||
if number <= 0:
|
try:
|
||||||
|
float(number)
|
||||||
|
except ValueError:
|
||||||
|
return 0
|
||||||
|
if not number or number <= 0:
|
||||||
return number
|
return number
|
||||||
mag2suffix = {3: 'k', 6: 'M', 9: 'B'}
|
mag2suffix = {3: 'k', 6: 'M', 9: 'B'}
|
||||||
mag = math.floor(math.log(number, 10))
|
mag = math.floor(math.log(number, 10))
|
||||||
|
if mag < 3:
|
||||||
|
return number
|
||||||
maxmag = max(itertools.takewhile(lambda key: mag >= key, mag2suffix))
|
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):
|
def percent_change(init, new):
|
||||||
|
|
|
@ -5,11 +5,14 @@ Launch with ``piker watch <watchlist name>``.
|
||||||
|
|
||||||
(Currently there's a bunch of QT specific stuff in here)
|
(Currently there's a bunch of QT specific stuff in here)
|
||||||
"""
|
"""
|
||||||
|
from itertools import chain
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
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.button import Button
|
from kivy.uix.button import Button
|
||||||
from kivy.uix.label import Label
|
|
||||||
from kivy.uix.scrollview import ScrollView
|
from kivy.uix.scrollview import ScrollView
|
||||||
from kivy.lang import Builder
|
from kivy.lang import Builder
|
||||||
from kivy import utils
|
from kivy import utils
|
||||||
|
@ -38,79 +41,75 @@ def colorcode(name):
|
||||||
return _colors[name if name else 'gray']
|
return _colors[name if name else 'gray']
|
||||||
|
|
||||||
|
|
||||||
|
_bs = 3 # border size
|
||||||
|
_color = [0.13]*3 # nice shade of gray
|
||||||
_kv = (f'''
|
_kv = (f'''
|
||||||
#:kivy 1.10.0
|
#:kivy 1.10.0
|
||||||
|
|
||||||
<Cell>
|
<Cell>
|
||||||
|
font_size: 18
|
||||||
text_size: self.size
|
text_size: self.size
|
||||||
size: self.texture_size
|
size: self.texture_size
|
||||||
font_size: '20'
|
|
||||||
color: {colorcode('gray')}
|
color: {colorcode('gray')}
|
||||||
# size_hint_y: None
|
|
||||||
font_color: {colorcode('gray')}
|
font_color: {colorcode('gray')}
|
||||||
font_name: 'Roboto-Regular'
|
font_name: 'Roboto-Regular'
|
||||||
# height: 50
|
background_color: [0.13]*3 + [1]
|
||||||
# width: 50
|
background_normal: ''
|
||||||
background_color: [0]*4
|
|
||||||
valign: 'middle'
|
valign: 'middle'
|
||||||
halign: 'center'
|
halign: 'center'
|
||||||
outline_color: [0.1]*4
|
outline_color: [0.1]*4
|
||||||
canvas.before:
|
|
||||||
Color:
|
|
||||||
rgb: [0.08]*4
|
|
||||||
Rectangle:
|
|
||||||
pos: self.pos
|
|
||||||
size: self.size
|
|
||||||
|
|
||||||
<HeaderCell>
|
<HeaderCell>
|
||||||
# bold: True
|
font_size: 20
|
||||||
font_size: '20'
|
|
||||||
background_color: [0]*4
|
background_color: [0]*4
|
||||||
canvas.before:
|
canvas.before:
|
||||||
Color:
|
Color:
|
||||||
rgb: [0.13]*4
|
rgb: {_color}
|
||||||
Rectangle:
|
BorderImage: # use a fixed size border
|
||||||
pos: self.pos
|
pos: self.pos
|
||||||
size: self.size
|
size: [self.size[0] - {_bs}, self.size[1]]
|
||||||
# RoundedRectangle:
|
# 0s are because the containing TickerTable already has spacing
|
||||||
# pos: self.pos
|
border: [0, {_bs} , 0, {_bs}]
|
||||||
# size: self.size
|
|
||||||
# radius: [8,]
|
|
||||||
|
|
||||||
<TickerTable>
|
<TickerTable>
|
||||||
spacing: '5dp'
|
spacing: '{_bs}dp'
|
||||||
row_force_default: True
|
row_force_default: True
|
||||||
row_default_height: 75
|
row_default_height: 75
|
||||||
cols: 1
|
cols: 1
|
||||||
|
|
||||||
|
<BidAskLayout>
|
||||||
|
spacing: [{_bs}, 0]
|
||||||
|
|
||||||
<Row>
|
<Row>
|
||||||
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: 75
|
||||||
outline_color: [.7]*4
|
outline_color: [.7]*4
|
||||||
''')
|
''')
|
||||||
|
|
||||||
|
|
||||||
# Questrade key conversion
|
# Questrade key conversion / column order
|
||||||
_qt_keys = {
|
_qt_keys = {
|
||||||
# 'symbol': 'symbol', # done manually in qtconvert
|
'symbol': 'symbol', # done manually in qtconvert
|
||||||
|
'%': '%',
|
||||||
'lastTradePrice': 'last',
|
'lastTradePrice': 'last',
|
||||||
'askPrice': 'ask',
|
'askPrice': 'ask',
|
||||||
'bidPrice': 'bid',
|
'bidPrice': 'bid',
|
||||||
'lastTradeSize': 'last size',
|
'lastTradeSize': 'size',
|
||||||
'bidSize': 'bid size',
|
'bidSize': 'bsize',
|
||||||
'askSize': 'ask size',
|
'askSize': 'asize',
|
||||||
|
'VWAP': ('VWAP', partial(round, ndigits=3)),
|
||||||
'volume': ('vol', humanize),
|
'volume': ('vol', humanize),
|
||||||
'VWAP': ('VWAP', "{:.3f}".format),
|
'mktcap': ('mktcap', humanize),
|
||||||
'high52w': 'high52w',
|
'openPrice': 'open',
|
||||||
|
'lowPrice': 'low',
|
||||||
'highPrice': 'high',
|
'highPrice': 'high',
|
||||||
|
'low52w': 'low52w',
|
||||||
|
'high52w': 'high52w',
|
||||||
# "lastTradePriceTrHrs": 7.99,
|
# "lastTradePriceTrHrs": 7.99,
|
||||||
# "lastTradeTick": "Equal",
|
# "lastTradeTick": "Equal",
|
||||||
# "lastTradeTime": "2018-01-30T18:28:23.434000-05:00",
|
# "lastTradeTime": "2018-01-30T18:28:23.434000-05:00",
|
||||||
# 'low52w': 'low52w',
|
|
||||||
'lowPrice': 'low day',
|
|
||||||
'openPrice': 'open',
|
|
||||||
# "symbolId": 3575753,
|
# "symbolId": 3575753,
|
||||||
# "tier": "",
|
# "tier": "",
|
||||||
# 'isHalted': 'halted',
|
# 'isHalted': 'halted',
|
||||||
|
@ -128,19 +127,25 @@ def qtconvert(
|
||||||
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
|
||||||
"display-friendly" string format.
|
"display-friendly" string format.
|
||||||
"""
|
"""
|
||||||
|
last = quote['lastTradePrice']
|
||||||
|
symbol = quote['symbol']
|
||||||
if symbol_data: # we can only compute % change from symbols data
|
if symbol_data: # we can only compute % change from symbols data
|
||||||
previous = symbol_data[quote['symbol']]['prevDayClosePrice']
|
previous = symbol_data[symbol]['prevDayClosePrice']
|
||||||
change = percent_change(previous, quote['lastTradePrice'])
|
change = percent_change(previous, last)
|
||||||
|
share_count = symbol_data[symbol].get('outstandingShares', None)
|
||||||
|
mktcap = share_count * last if share_count else 'NA'
|
||||||
else:
|
else:
|
||||||
change = 0
|
change = 0
|
||||||
new = {
|
computed = {
|
||||||
'symbol': quote['symbol'],
|
'symbol': quote['symbol'],
|
||||||
'%': round(change, 3)
|
'%': round(change, 3),
|
||||||
|
'mktcap': mktcap,
|
||||||
}
|
}
|
||||||
displayable = new.copy()
|
new = {}
|
||||||
|
displayable = {}
|
||||||
|
|
||||||
for key, new_key in keymap.items():
|
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)
|
# 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
|
||||||
|
@ -162,10 +167,11 @@ class HeaderCell(Button):
|
||||||
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 rows by this column
|
||||||
# in `update_quotes()`
|
# in `update_quotes()`
|
||||||
|
table = self.row.table
|
||||||
if self.row.is_header:
|
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:
|
if last and last is not self:
|
||||||
last.underline = False
|
last.underline = False
|
||||||
last.bold = False
|
last.bold = False
|
||||||
|
@ -174,7 +180,9 @@ class HeaderCell(Button):
|
||||||
self.underline = True
|
self.underline = True
|
||||||
self.bold = True
|
self.bold = True
|
||||||
# mark this cell as the last
|
# 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
|
# allow highlighting of row headers for tracking
|
||||||
elif self.is_header:
|
elif self.is_header:
|
||||||
|
@ -184,11 +192,62 @@ class HeaderCell(Button):
|
||||||
self.background_color = self.color
|
self.background_color = self.color
|
||||||
|
|
||||||
|
|
||||||
class Cell(Label):
|
class Cell(Button):
|
||||||
"""Data cell label.
|
"""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):
|
class Row(GridLayout):
|
||||||
"""A grid for displaying a row of ticker quote data.
|
"""A grid for displaying a row of ticker quote data.
|
||||||
|
|
||||||
|
@ -205,12 +264,40 @@ class Row(GridLayout):
|
||||||
self.table = table
|
self.table = table
|
||||||
self.is_header = is_header_row
|
self.is_header = is_header_row
|
||||||
|
|
||||||
|
# create `BidAskCells` first
|
||||||
|
bidasks = {
|
||||||
|
'last': ['bid', 'ask'],
|
||||||
|
'size': ['bsize', 'asize']
|
||||||
|
}
|
||||||
|
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
|
# build out row using Cell labels
|
||||||
for (key, val) in record.items():
|
for (key, val) in record.items():
|
||||||
header = key in headers
|
header = key in headers
|
||||||
cell = self._append_cell(val, header=header)
|
|
||||||
self._cell_widgets[key] = cell
|
# handle bidask cells
|
||||||
cell.key = key
|
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):
|
def get_cell(self, key):
|
||||||
return self._cell_widgets[key]
|
return self._cell_widgets[key]
|
||||||
|
@ -224,22 +311,35 @@ class Row(GridLayout):
|
||||||
cell = celltype(text=str(text))
|
cell = celltype(text=str(text))
|
||||||
cell.is_header = header
|
cell.is_header = header
|
||||||
cell.row = self
|
cell.row = self
|
||||||
|
|
||||||
# don't bold the header row
|
|
||||||
if header and self.is_header:
|
|
||||||
cell.bold = False
|
|
||||||
|
|
||||||
self.add_widget(cell)
|
self.add_widget(cell)
|
||||||
return 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):
|
class TickerTable(GridLayout):
|
||||||
"""A grid for displaying ticker quote records as a table.
|
"""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)
|
super(TickerTable, self).__init__(**kwargs)
|
||||||
self.symbols2rows = {}
|
self.symbols2rows = {}
|
||||||
self.sort_key = sort_key
|
self.sort_key = sort_key
|
||||||
|
self.quote_cache = quote_cache
|
||||||
# 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
|
||||||
|
|
||||||
|
@ -252,6 +352,16 @@ class TickerTable(GridLayout):
|
||||||
self.add_widget(row)
|
self.add_widget(row)
|
||||||
return 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):
|
def header_row(headers, **kwargs):
|
||||||
"""Create a single "header" row from a sequence of keys.
|
"""Create a single "header" row from a sequence of keys.
|
||||||
|
@ -281,8 +391,8 @@ async def update_quotes(
|
||||||
grid = widgets['grid']
|
grid = widgets['grid']
|
||||||
|
|
||||||
def color_row(row, data):
|
def color_row(row, data):
|
||||||
hdrcell = row._cell_widgets['symbol']
|
hdrcell = row.get_cell('symbol')
|
||||||
chngcell = row._cell_widgets['%']
|
chngcell = row.get_cell('%')
|
||||||
daychange = float(data['%'])
|
daychange = float(data['%'])
|
||||||
if daychange < 0.:
|
if daychange < 0.:
|
||||||
color = colorcode('red2')
|
color = colorcode('red2')
|
||||||
|
@ -295,61 +405,41 @@ async def update_quotes(
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
cache = {}
|
cache = {}
|
||||||
|
grid.quote_cache = cache
|
||||||
|
|
||||||
# initial coloring
|
# initial coloring
|
||||||
for quote in first_quotes:
|
for quote in first_quotes:
|
||||||
sym = quote['symbol']
|
sym = quote['symbol']
|
||||||
row = grid.symbols2rows[sym]
|
row = grid.symbols2rows[sym]
|
||||||
data, _ = qtconvert(quote, symbol_data=symbol_data)
|
record, displayable = qtconvert(quote, symbol_data=symbol_data)
|
||||||
color_row(row, data)
|
row.update(record, displayable)
|
||||||
cache[sym] = (data, row)
|
color_row(row, record)
|
||||||
|
cache[sym] = (record, row)
|
||||||
|
|
||||||
# the core cell update loop
|
grid.render_rows(cache)
|
||||||
|
|
||||||
|
# core cell update loop
|
||||||
while True:
|
while True:
|
||||||
log.debug("Waiting on quotes")
|
log.debug("Waiting on quotes")
|
||||||
quotes = await queue.get()
|
quotes = await queue.get() # new quotes data only
|
||||||
for quote in quotes:
|
for quote in quotes:
|
||||||
data, displayable = qtconvert(quote, symbol_data=symbol_data)
|
record, displayable = qtconvert(quote, symbol_data=symbol_data)
|
||||||
row = grid.symbols2rows[data['symbol']]
|
row = grid.symbols2rows[record['symbol']]
|
||||||
# only updates newly timestamped quotes
|
cache[record['symbol']] = (record, row)
|
||||||
cache[data['symbol']] = (data, row)
|
row.update(record, displayable)
|
||||||
|
color_row(row, record)
|
||||||
|
|
||||||
# color changed field values
|
grid.render_rows(cache)
|
||||||
for key, val in data.items():
|
|
||||||
# logic for cell text coloring: up-green, down-red
|
|
||||||
if row._last_record[key] < val:
|
|
||||||
color = colorcode('green')
|
|
||||||
elif row._last_record[key] > val:
|
|
||||||
color = colorcode('red')
|
|
||||||
else:
|
|
||||||
color = colorcode('gray')
|
|
||||||
|
|
||||||
cell = row._cell_widgets[key]
|
|
||||||
cell.text = str(displayable[key])
|
|
||||||
cell.color = color
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
async def run_kivy(root, nursery):
|
async def run_kivy(root, nursery):
|
||||||
'''Trio-kivy entry point.
|
'''Trio-kivy entry point.
|
||||||
'''
|
'''
|
||||||
# run kivy
|
await async_runTouchApp(root) # run kivy
|
||||||
await async_runTouchApp(root)
|
nursery.cancel_scope.cancel() # cancel all other tasks that may be running
|
||||||
# now cancel all the other tasks that may be running
|
|
||||||
nursery.cancel_scope.cancel()
|
|
||||||
|
|
||||||
|
|
||||||
async def _async_main(name, watchlists, brokermod):
|
async def _async_main(name, watchlists, brokermod):
|
||||||
|
@ -380,19 +470,16 @@ async def _async_main(name, watchlists, brokermod):
|
||||||
|
|
||||||
# build out UI
|
# build out UI
|
||||||
Builder.load_string(_kv)
|
Builder.load_string(_kv)
|
||||||
root = BoxLayout(orientation='vertical', padding=5, spacing=-20)
|
root = BoxLayout(orientation='vertical', padding=5, spacing=5)
|
||||||
header = header_row(
|
header = header_row(
|
||||||
first_quotes[0].keys(),
|
first_quotes[0].keys(),
|
||||||
size_hint=(1, None),
|
size_hint=(1, None),
|
||||||
# put black lines between cells on the header row
|
|
||||||
spacing='3dp',
|
|
||||||
)
|
)
|
||||||
root.add_widget(header)
|
root.add_widget(header)
|
||||||
grid = ticker_table(
|
grid = ticker_table(
|
||||||
first_quotes,
|
first_quotes,
|
||||||
size_hint=(1, None),
|
size_hint=(1, None),
|
||||||
)
|
)
|
||||||
|
|
||||||
# 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
|
||||||
|
@ -401,6 +488,7 @@ async def _async_main(name, watchlists, brokermod):
|
||||||
sort_cell.bold = sort_cell.underline = True
|
sort_cell.bold = sort_cell.underline = True
|
||||||
grid.last_clicked_col_cell = sort_cell
|
grid.last_clicked_col_cell = sort_cell
|
||||||
|
|
||||||
|
# set up a scroll view for large ticker lists
|
||||||
grid.bind(minimum_height=grid.setter('height'))
|
grid.bind(minimum_height=grid.setter('height'))
|
||||||
scroll = ScrollView()
|
scroll = ScrollView()
|
||||||
scroll.add_widget(grid)
|
scroll.add_widget(grid)
|
||||||
|
@ -412,6 +500,5 @@ async def _async_main(name, watchlists, brokermod):
|
||||||
'header': header,
|
'header': header,
|
||||||
'scroll': scroll,
|
'scroll': scroll,
|
||||||
}
|
}
|
||||||
|
|
||||||
nursery.start_soon(run_kivy, widgets['root'], nursery)
|
nursery.start_soon(run_kivy, widgets['root'], nursery)
|
||||||
nursery.start_soon(update_quotes, widgets, queue, sd, pkts)
|
nursery.start_soon(update_quotes, widgets, queue, sd, pkts)
|
||||||
|
|
Loading…
Reference in New Issue