Separate sortable and dislplayable quote values

kivy_mainline_and_py3.8
Tyler Goodlet 2018-02-10 19:54:09 -05:00
parent 488f3988ea
commit f4fd35fa21
2 changed files with 69 additions and 46 deletions

View File

@ -9,7 +9,16 @@ 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:
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))
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 "{:.3f}{}".format(number/10**maxmag, mag2suffix[maxmag])
def percent_change(init, new):
"""Calcuate the percentage change of some ``new`` value
from some initial value, ``init``.
"""
return (new - init) / init * 100.

View File

@ -6,7 +6,6 @@ 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)
""" """
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.button import Button from kivy.uix.button import Button
@ -17,28 +16,19 @@ from kivy import utils
from kivy.app import async_runTouchApp from kivy.app import async_runTouchApp
from ..calc import humanize from ..calc import humanize, percent_change
from ..log import get_logger from ..log import get_logger
log = get_logger('watchlist') log = get_logger('watchlist')
def same_rgb(val):
return ', '.join(map(str, [val]*3))
_colors2hexs = { _colors2hexs = {
'darkgray': 'a9a9a9', 'darkgray': 'a9a9a9',
'gray': '808080', 'gray': '808080',
'green': '008000', 'green': '008000',
'forestgreen': '228b22', 'forestgreen': '228b22',
'seagreen': '2e8b57',
'red2': 'ff3333', 'red2': 'ff3333',
'red': 'ff0000', 'red': 'ff0000',
'tomato': 'ff6347',
'darkred': '8b0000',
'firebrick': 'b22222', 'firebrick': 'b22222',
'maroon': '800000',
'gainsboro': 'dcdcdc',
} }
_colors = {key: utils.rgba(val) for key, val in _colors2hexs.items()} _colors = {key: utils.rgba(val) for key, val in _colors2hexs.items()}
@ -54,35 +44,37 @@ _kv = (f'''
<Cell> <Cell>
text_size: self.size text_size: self.size
size: self.texture_size size: self.texture_size
font_size: '18sp' font_size: '20'
# size_hint_y: None # size_hint_y: None
font_color: {colorcode('gray')} font_color: {colorcode('gray')}
font_name: 'Roboto-Regular' font_name: 'Roboto-Regular'
# height: 50 # height: 50
# width: 50 # width: 50
background_color: 0,0,0,0 background_color: [0]*4
valign: 'middle' valign: 'middle'
halign: 'center' halign: 'center'
# outline_color: {same_rgb(0.001)} outline_color: [0.1]*4
canvas.before: canvas.before:
Color: Color:
rgb: {same_rgb(0.05)} rgb: [0.08]*4
RoundedRectangle: Rectangle:
pos: self.pos pos: self.pos
size: self.size size: self.size
radius: [7,]
<HeaderCell> <HeaderCell>
bold: True bold: True
font_size: '18sp' font_size: '20'
background_color: 0,0,0,0 background_color: 0,0,0,0
canvas.before: canvas.before:
Color: Color:
rgb: {same_rgb(0.12)} rgb: [0.13]*4
RoundedRectangle: Rectangle:
pos: self.pos pos: self.pos
size: self.size size: self.size
radius: [7,] # RoundedRectangle:
# pos: self.pos
# size: self.size
# radius: [8,]
<TickerTable> <TickerTable>
spacing: '5dp' spacing: '5dp'
@ -91,15 +83,15 @@ _kv = (f'''
cols: 1 cols: 1
<Row> <Row>
spacing: '6dp'
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: {same_rgb(.7)} outline_color: [.7]*4
''') ''')
# Questrade key conversion
_qt_keys = { _qt_keys = {
# 'symbol': 'symbol', # done manually in qtconvert # 'symbol': 'symbol', # done manually in qtconvert
'lastTradePrice': 'last', 'lastTradePrice': 'last',
@ -125,28 +117,42 @@ _qt_keys = {
} }
def qtconvert(quote: dict, keymap: dict = _qt_keys, symbol_data: dict = None): def qtconvert(
"""Remap a list of quote dicts ``quotes`` using quote: dict, keymap: dict = _qt_keys, symbol_data: dict = None
the mapping of old keys -> new keys ``keymap``. ) -> (dict, dict):
"""Remap a list of quote dicts ``quotes`` using the mapping of old keys
-> new keys ``keymap``.
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
"display-friendly" string format.
""" """
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[quote['symbol']]['prevDayClosePrice']
change = (quote['lastTradePrice'] - previous) / previous * 100 change = percent_change(previous, quote['lastTradePrice'])
else: else:
change = 0 change = 0
new = { new = {
'symbol': quote['symbol'], 'symbol': quote['symbol'],
'%': f"{change:.2f}" '%': round(change, 3)
} }
displayable = new.copy()
for key, new_key in keymap.items(): for key, new_key in keymap.items():
value = quote[key] display_value = value = quote[key]
# API servers can return `None` vals when markets are closed (weekend)
value = 0 if value is None else value
# convert values to a displayble format
if isinstance(new_key, tuple): if isinstance(new_key, tuple):
new_key, func = new_key new_key, func = new_key
value = func(value) display_value = func(value)
new[new_key] = value new[new_key] = value
displayable[new_key] = display_value
return new return new, displayable
class HeaderCell(Button): class HeaderCell(Button):
@ -158,7 +164,7 @@ class HeaderCell(Button):
if self.row.is_header: if self.row.is_header:
self.row.table.sort_key = self.key self.row.table.sort_key = self.key
last = self.row.table._last_clicked_col_cell last = self.row.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
@ -167,10 +173,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 self.row.table.last_clicked_col_cell = self
# allow highlighting row headers for visual following of # allow highlighting of row headers for tracking
# specific tickers
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 = [0]*4
@ -200,7 +205,7 @@ class Row(GridLayout):
self.is_header = is_header_row self.is_header = is_header_row
# 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) cell = self._append_cell(val, header=header)
self._cell_widgets[key] = cell self._cell_widgets[key] = cell
@ -209,15 +214,20 @@ class Row(GridLayout):
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, colorname=None, header=False): def _append_cell(self, text, 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), color=colorcode(colorname)) 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
@ -230,7 +240,7 @@ class TickerTable(GridLayout):
self.symbols2rows = {} self.symbols2rows = {}
self.sort_key = sort_key self.sort_key = sort_key
# 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
def append_row(self, record): def append_row(self, record):
"""Append a `Row` of `Cell` objects to this table. """Append a `Row` of `Cell` objects to this table.
@ -294,12 +304,13 @@ async def update_quotes(
syms2rows[sym] = row syms2rows[sym] = row
color_row(row, quote) color_row(row, quote)
# the 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()
datas = [] datas = []
for quote in quotes: for quote in quotes:
data = qtconvert(quote, symbol_data=symbol_data) data, displayable = qtconvert(quote, symbol_data=symbol_data)
row = grid.symbols2rows[data['symbol']] row = grid.symbols2rows[data['symbol']]
datas.append((data, row)) datas.append((data, row))
@ -314,7 +325,7 @@ async def update_quotes(
color = colorcode('gray') color = colorcode('gray')
cell = row._cell_widgets[key] cell = row._cell_widgets[key]
cell.text = str(val) cell.text = str(displayable[key])
cell.color = color cell.color = color
color_row(row, data) color_row(row, data)
@ -327,8 +338,6 @@ async def update_quotes(
reversed(sorted(datas, key=lambda item: item[0][sort_key])) reversed(sorted(datas, key=lambda item: item[0][sort_key]))
): ):
grid.add_widget(row) # row append grid.add_widget(row) # row append
# print(f'{i} {data["symbol"]} {data["%"]}')
# await trio.sleep(0.1)
async def run_kivy(root, nursery): async def run_kivy(root, nursery):
@ -342,6 +351,8 @@ async def run_kivy(root, nursery):
async def _async_main(name, watchlists, brokermod): async def _async_main(name, watchlists, brokermod):
'''Launch kivy app + all other related tasks. '''Launch kivy app + all other related tasks.
This is started with cli command `piker watch`.
''' '''
tickers = watchlists[name] tickers = watchlists[name]
queue = trio.Queue(1000) queue = trio.Queue(1000)
@ -362,7 +373,7 @@ async def _async_main(name, watchlists, brokermod):
return return
first_quotes = [ first_quotes = [
qtconvert(quote, symbol_data=sd) for quote in pkts] qtconvert(quote, symbol_data=sd)[0] for quote in pkts]
# build out UI # build out UI
Builder.load_string(_kv) Builder.load_string(_kv)
@ -370,6 +381,8 @@ async def _async_main(name, watchlists, brokermod):
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(
@ -377,12 +390,13 @@ async def _async_main(name, watchlists, brokermod):
size_hint=(1, None), size_hint=(1, None),
) )
# associate the col headers row with the ticker table even # associate the col headers row with the ticker table even though
# 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
grid.last_clicked_col_cell = sort_cell
grid.bind(minimum_height=grid.setter('height')) grid.bind(minimum_height=grid.setter('height'))
scroll = ScrollView() scroll = ScrollView()