Watchlist fixes

- make the % daily change use the previous days close as the reference
  price
- color each cell on every change (results in "pulsed" colors on changes)
- tweak some quote fields
- redraw and sort all rows on every quotes update cycle
- error when the QT api is returning None values
kivy_mainline_and_py3.8
Tyler Goodlet 2018-02-08 19:30:09 -05:00
parent 17c4ac3b8c
commit e45c07dce7
1 changed files with 95 additions and 67 deletions

View File

@ -7,24 +7,21 @@ from importlib import import_module
import click import click
import trio import trio
from ..log import get_logger, get_console_log
log = get_logger('watchlist')
# use the trio async loop # use the trio async loop
os.environ['KIVY_EVENTLOOP'] = 'trio' os.environ['KIVY_EVENTLOOP'] = 'trio'
from kivy.uix.widget import Widget
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.label import Label from kivy.uix.label import Label
from kivy.uix.scrollview import ScrollView from kivy.uix.scrollview import ScrollView
from kivy.core.window import Window from kivy.core.window import Window
from kivy.properties import DictProperty
from kivy.lang import Builder from kivy.lang import Builder
from kivy import utils from kivy import utils
from kivy.app import async_runTouchApp from kivy.app import async_runTouchApp
from ..log import get_logger, get_console_log
log = get_logger('watchlist')
def same_rgb(val): def same_rgb(val):
return ', '.join(map(str, [val]*3)) return ', '.join(map(str, [val]*3))
@ -32,14 +29,17 @@ def same_rgb(val):
def colorcode(name): def colorcode(name):
if not name: if not name:
name = 'darkgray' name = 'gray'
_names2hexs = { _names2hexs = {
'darkgray': 'a9a9a9', 'darkgray': 'a9a9a9',
'gray': '808080',
'green': '008000', 'green': '008000',
'red': 'ff3333', 'red2': 'ff3333',
'red2': 'ff0000', 'red': 'ff0000',
'dark_red': '8b0000', 'dark_red': '8b0000',
'firebrick': 'b22222', 'firebrick': 'b22222',
'maroon': '800000',
'gainsboro': 'dcdcdc',
} }
return utils.rgba(_names2hexs[name]) return utils.rgba(_names2hexs[name])
@ -48,12 +48,12 @@ _kv = (f'''
#:kivy 1.10.0 #:kivy 1.10.0
<HeaderCell> <HeaderCell>
# font_size: '15' # font_size: 18
size: self.texture_size size: self.texture_size
# size_hint_y: None # size_hint_y: None
# height: '100dp' # height: 50
outline_color: {same_rgb(0.01)} outline_color: {same_rgb(0.01)}
width: '100dp' width: 50
valign: 'middle' valign: 'middle'
halign: 'center' halign: 'center'
canvas.before: canvas.before:
@ -67,14 +67,14 @@ _kv = (f'''
text_size: self.size text_size: self.size
size: self.texture_size size: self.texture_size
# font_size: '15' # font_size: '15'
font_color: {colorcode('darkgray')} font_color: {colorcode('gray')}
# font_name: 'sans serif' # font_name: 'sans serif'
valign: 'middle' valign: 'middle'
halign: 'center' halign: 'center'
# outline_color: {same_rgb(0.01)} # outline_color: {same_rgb(0.01)}
canvas.before: canvas.before:
Color: Color:
rgb: {same_rgb(0.05)} rgb: {same_rgb(0.06)}
Rectangle: Rectangle:
pos: self.pos pos: self.pos
size: self.size size: self.size
@ -88,7 +88,7 @@ _kv = (f'''
cols: 1 cols: 1
<Row> <Row>
spacing: '4dp' 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
@ -103,34 +103,38 @@ _qt_keys = {
'lastTradePrice': 'last', 'lastTradePrice': 'last',
'lastTradeSize': 'last size', 'lastTradeSize': 'last size',
'askPrice': 'ask', 'askPrice': 'ask',
'askSize': 'ask price', 'askSize': 'ask size',
'bidPrice': 'bid', 'bidPrice': 'bid',
'bidSize': 'bid size', 'bidSize': 'bid size',
'volume': 'vol', 'volume': 'vol',
'VWAP': 'vwap', 'VWAP': 'VWAP',
'high52w': 'high52w', 'high52w': 'high52w',
'highPrice': 'high', 'highPrice': 'high',
# "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', # 'low52w': 'low52w',
'lowPrice': 'low day', 'lowPrice': 'low day',
'openPrice': 'open', 'openPrice': 'open',
# "symbolId": 3575753, # "symbolId": 3575753,
# "tier": "", # "tier": "",
'isHalted': 'halted', # 'isHalted': 'halted',
'delay': 'delay', # as subscript 'p' # 'delay': 'delay', # as subscript 'p'
} }
def remap_keys(quote, keymap=_qt_keys): def remap_keys(quote: dict, keymap: dict = _qt_keys, symbol_data: dict = None):
"""Remap a list of quote dicts ``quotes`` using """Remap a list of quote dicts ``quotes`` using
the mapping of old keys -> new keys ``keymap``. the mapping of old keys -> new keys ``keymap``.
""" """
open_price = quote['openPrice'] if symbol_data: # we can only compute % change from symbols data
previous = symbol_data[quote['symbol']]['prevDayClosePrice']
change = (quote['lastTradePrice'] - previous) / previous * 100
else:
change = 0
new = { new = {
'symbol': quote['symbol'], 'symbol': quote['symbol'],
'%': f"{(quote['lastTradePrice'] - open_price) / open_price:10.2f}" '%': f"{change:.2f}"
} }
for key, new_key in keymap.items(): for key, new_key in keymap.items():
value = quote[key] value = quote[key]
@ -205,59 +209,73 @@ def ticker_table(quotes, **kwargs):
"""Create a new ticker table from a list of quote dicts. """Create a new ticker table from a list of quote dicts.
""" """
table = TickerTable(cols=1) table = TickerTable(cols=1)
for ticker_record in quotes: for ticker_record in quotes:
table.append_row(ticker_record) table.append_row(ticker_record)
return table return table
async def update_quotes(widgets, queue): async def update_quotes(
widgets: dict,
queue: trio.Queue,
symbol_data: dict,
first_quotes: dict
):
"""Process live quotes by updating ticker rows. """Process live quotes by updating ticker rows.
""" """
grid = widgets['grid'] grid = widgets['grid']
def color_row(row, data):
hdrcell = row._cell_widgets['symbol']
chngcell = row._cell_widgets['%']
daychange = float(data['%'])
if daychange < 0.:
color = colorcode('red')
elif daychange > 0.:
color = colorcode('green')
else:
color = colorcode('gray')
chngcell.color = hdrcell.color = color
# initial coloring
all_rows = []
for quote in first_quotes:
row = grid.symbols2rows[quote['symbol']]
all_rows.append((quote, row))
color_row(row, quote)
while True: while True:
log.debug("Waiting on quotes") log.debug("Waiting on quotes")
quotes = await queue.get() quotes = await queue.get()
rows = []
for quote in quotes: for quote in quotes:
data = remap_keys(quote) data = remap_keys(quote, symbol_data=symbol_data)
row = grid.symbols2rows[data['symbol']] row = grid.symbols2rows[data['symbol']]
rows.append((data, row))
new = set(data.items()) - set(row._last_record.items())
if new:
for key, val in filter(lambda item: item[0] != '%', new):
# logic for value coloring: up-green, down-red
if row._last_record[key] < val:
color = colorcode('green')
elif row._last_record[key] > val:
color = colorcode('red2')
cell = row._cell_widgets[key] # color changed field values
cell.text = str(val) for key, val in data.items():
cell.color = color # 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')
row._last_record = data cell = row._cell_widgets[key]
cell.text = str(val)
cell.color = color
hdrcell = row._cell_widgets['symbol'] color_row(row, data)
chngcell = row._cell_widgets['%'] row._last_record = data
daychange = float(data['%'])
if daychange < 0.:
color = colorcode('red2')
chngcell.color = hdrcell.color = color
elif daychange > 0.:
color = colorcode('green')
chngcell.color = hdrcell.color = color
# sort rows by % change # sort rows by daily % change since open
for i, pair in enumerate( grid.clear_widgets()
sorted(rows, key=lambda item: float(item[0]['%'])) for i, (data, row) in enumerate(
sorted(all_rows, key=lambda item: float(item[0]['%']))
): ):
data, row = pair # print(f"{i} {data['symbol']}")
if grid.children[i] != row: # grid.remove_widget(row)
grid.remove_widget(row) grid.add_widget(row, index=i)
grid.add_widget(row, index=i)
async def run_kivy(root, nursery): async def run_kivy(root, nursery):
@ -276,23 +294,30 @@ async def _async_main(tickers, brokermod):
async with brokermod.get_client() as client: async with brokermod.get_client() as client:
async with trio.open_nursery() as nursery: async with trio.open_nursery() as nursery:
# get long term data including last days close price
sd = await client.symbols(tickers)
nursery.start_soon(brokermod.poll_tickers, client, tickers, queue) nursery.start_soon(brokermod.poll_tickers, client, tickers, queue)
# get first quotes response # get first quotes response
quotes = []
pkts = await queue.get() pkts = await queue.get()
for quote in pkts:
quotes.append(remap_keys(quote)) if pkts[0]['lastTradePrice'] is None:
log.error("Questrade API is down temporarily")
nursery.cancel_scope.cancel()
return
first_quotes = [
remap_keys(quote, symbol_data=sd) for quote in pkts]
# build out UI # build out UI
Builder.load_string(_kv) Builder.load_string(_kv)
root = BoxLayout(orientation='vertical') root = BoxLayout(orientation='vertical')
header = header_row(quotes[0].keys()) header = header_row(first_quotes[0].keys())
root.add_widget(header) root.add_widget(header)
grid = ticker_table(quotes) grid = ticker_table(first_quotes)
grid.bind(minimum_height=grid.setter('height')) grid.bind(minimum_height=grid.setter('height'))
scroll = ScrollView( scroll = ScrollView(bar_margin=10, viewport_size=(10, 10))
size=(Window.width, Window.height), bar_margin=10)
scroll.add_widget(grid) scroll.add_widget(grid)
root.add_widget(scroll) root.add_widget(scroll)
@ -304,7 +329,7 @@ async def _async_main(tickers, brokermod):
} }
nursery.start_soon(run_kivy, widgets['root'], nursery) nursery.start_soon(run_kivy, widgets['root'], nursery)
nursery.start_soon(update_quotes, widgets, queue) nursery.start_soon(update_quotes, widgets, queue, sd, first_quotes)
@click.group() @click.group()
@ -324,8 +349,11 @@ def run(loglevel, broker):
watchlists = { watchlists = {
'cannabis': [ 'cannabis': [
'EMH.VN', 'LEAF.TO', 'HVT.VN', 'HMMJ.TO', 'APH.TO', 'EMH.VN', 'LEAF.TO', 'HVT.VN', 'HMMJ.TO', 'APH.TO',
'CBW.VN', 'TRST.CN', 'VFF.TO', 'ACB.TO', 'ABCN.VN' 'CBW.VN', 'TRST.CN', 'VFF.TO', 'ACB.TO', 'ABCN.VN',
'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',
'OGI.VN', 'IMH.VN', 'FIRE.VN', 'EAT.CN', 'NUU.VN',
'WMD.VN', 'HEMP.VN', 'CALI.CN', 'RBQ.CN',
], ],
} }
# broker_conf_path = os.path.join(click.get_app_dir('piker'), 'watchlists.json') # broker_conf_path = os.path.join(click.get_app_dir('piker'), 'watchlists.json')