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 valueskivy_mainline_and_py3.8
parent
17c4ac3b8c
commit
e45c07dce7
|
@ -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,58 +209,72 @@ 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())
|
# color changed field values
|
||||||
if new:
|
for key, val in data.items():
|
||||||
for key, val in filter(lambda item: item[0] != '%', new):
|
# logic for cell text coloring: up-green, down-red
|
||||||
# logic for value coloring: up-green, down-red
|
|
||||||
if row._last_record[key] < val:
|
if row._last_record[key] < val:
|
||||||
color = colorcode('green')
|
color = colorcode('green')
|
||||||
elif row._last_record[key] > val:
|
elif row._last_record[key] > val:
|
||||||
color = colorcode('red2')
|
color = colorcode('red')
|
||||||
|
else:
|
||||||
|
color = colorcode('gray')
|
||||||
|
|
||||||
cell = row._cell_widgets[key]
|
cell = row._cell_widgets[key]
|
||||||
cell.text = str(val)
|
cell.text = str(val)
|
||||||
cell.color = color
|
cell.color = color
|
||||||
|
|
||||||
|
color_row(row, data)
|
||||||
row._last_record = data
|
row._last_record = data
|
||||||
|
|
||||||
hdrcell = row._cell_widgets['symbol']
|
# sort rows by daily % change since open
|
||||||
chngcell = row._cell_widgets['%']
|
grid.clear_widgets()
|
||||||
daychange = float(data['%'])
|
for i, (data, row) in enumerate(
|
||||||
if daychange < 0.:
|
sorted(all_rows, key=lambda item: float(item[0]['%']))
|
||||||
color = colorcode('red2')
|
|
||||||
chngcell.color = hdrcell.color = color
|
|
||||||
elif daychange > 0.:
|
|
||||||
color = colorcode('green')
|
|
||||||
chngcell.color = hdrcell.color = color
|
|
||||||
|
|
||||||
# sort rows by % change
|
|
||||||
for i, pair in enumerate(
|
|
||||||
sorted(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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
|
Loading…
Reference in New Issue