piker/piker/ui/kivy/monitor.py

300 lines
8.9 KiB
Python
Raw Normal View History

2018-02-08 07:15:43 +00:00
"""
2018-08-24 03:12:39 +00:00
monitor: a real-time, sorted watchlist.
2018-08-24 03:12:39 +00:00
Launch with ``piker monitor <watchlist name>``.
2018-02-08 07:15:43 +00:00
2018-03-14 18:00:24 +00:00
(Currently there's a bunch of questrade specific stuff in here)
"""
2018-06-27 15:50:02 +00:00
from types import ModuleType, AsyncGeneratorType
from typing import List, Callable
2018-02-08 07:15:43 +00:00
import trio
import tractor
2018-02-08 07:15:43 +00:00
from kivy.uix.boxlayout import BoxLayout
from kivy.lang import Builder
from kivy.app import async_runTouchApp
2018-02-22 23:39:21 +00:00
from kivy.core.window import Window
2018-02-08 07:15:43 +00:00
from ...brokers.data import DataFeed
2019-01-03 02:12:42 +00:00
from .tabular import (
Row, TickerTable, _kv, _black_rgba, colorcode,
)
from ...log import get_logger
2018-02-22 23:52:58 +00:00
from .pager import PagerView
2018-02-22 23:39:21 +00:00
2018-08-24 03:12:39 +00:00
log = get_logger('monitor')
2018-02-09 07:44:43 +00:00
async def update_quotes(
nursery: trio._core._run.Nursery,
formatter: Callable,
widgets: dict,
2018-06-27 15:50:02 +00:00
agen: AsyncGeneratorType,
symbol_data: dict,
first_quotes: dict,
task_status: trio._core._run._TaskStatus = trio.TASK_STATUS_IGNORED,
):
2018-02-08 07:15:43 +00:00
"""Process live quotes by updating ticker rows.
"""
log.debug("Initializing UI update loop")
table = widgets['table']
flash_keys = {'low', 'high'}
async def revert_cells_color(cells):
await trio.sleep(0.3)
for cell in cells:
cell.background_color = _black_rgba
2018-02-08 07:15:43 +00:00
def color_row(row, record, cells):
hdrcell = row.get_cell('symbol')
chngcell = row.get_cell('%')
# determine daily change color
percent_change = record.get('%')
if percent_change is not None and percent_change != chngcell:
daychange = float(percent_change)
if daychange < 0.:
color = colorcode('red2')
elif daychange > 0.:
color = colorcode('forestgreen')
else:
color = colorcode('gray')
# if the cell has been "highlighted" make sure to change its color
2021-02-21 17:32:40 +00:00
if hdrcell.background_color != [0] * 4:
hdrcell.background_color = color
# update row header and '%' cell text color
chngcell.color = color
hdrcell.color = color
# briefly highlight bg of certain cells on each trade execution
unflash = set()
tick_color = None
last = cells.get('last')
if not last:
vol = cells.get('volume')
if not vol:
return # no trade exec took place
# flash gray on volume tick
# (means trade exec @ current price)
last = row.get_cell('last')
tick_color = colorcode('gray')
else:
tick_color = last.color
last.background_color = tick_color
unflash.add(last)
# flash the size cell
size = row.get_cell('size')
size.background_color = tick_color
unflash.add(size)
# flash all other cells
for key in flash_keys:
cell = cells.get(key)
if cell:
cell.background_color = cell.color
unflash.add(cell)
# revert flash state momentarily
nursery.start_soon(revert_cells_color, unflash)
# initial coloring
to_sort = set()
2020-08-10 19:48:57 +00:00
for quote in first_quotes:
row = table.get_row(quote['symbol'])
row.update(quote)
color_row(row, quote, {})
to_sort.add(row.widget)
table.render_rows(to_sort)
log.debug("Finished initializing update loop")
task_status.started()
2020-08-10 19:48:57 +00:00
2018-11-13 23:41:40 +00:00
# real-time cell update loop
2018-06-27 15:50:02 +00:00
async for quotes in agen: # new quotes data only
to_sort = set()
for symbol, quote in quotes.items():
row = table.get_row(symbol)
# don't red/green the header cell in ``row.update()``
2020-08-10 19:48:57 +00:00
quote.pop('symbol')
quote.pop('key')
# determine if sorting should happen
sort_key = table.sort_key
last = row.get_field(sort_key)
2020-08-10 19:48:57 +00:00
new = quote.get(sort_key, last)
if new != last:
to_sort.add(row.widget)
# update and color
2020-08-10 19:48:57 +00:00
cells = row.update(quote)
color_row(row, quote, cells)
if to_sort:
table.render_rows(to_sort)
log.debug("Waiting on quotes")
2018-02-08 07:15:43 +00:00
log.warn("Data feed connection dropped")
2018-02-08 07:15:43 +00:00
2018-04-19 04:27:04 +00:00
2021-02-21 17:32:40 +00:00
_widgets = {}
async def stream_symbol_selection():
"""An RPC async gen for streaming the symbol corresponding
value corresponding to the last clicked row.
Essentially of an event stream of clicked symbol values.
"""
2021-02-21 17:32:40 +00:00
global _widgets
table = _widgets['table']
send_chan, recv_chan = trio.open_memory_channel(0)
table._click_queues.append(send_chan)
try:
async with recv_chan:
async for symbol in recv_chan:
yield symbol
finally:
table._click_queues.remove(send_chan)
async def _async_main(
name: str,
portal: tractor.Portal,
2020-05-23 20:02:15 +00:00
symbols: List[str],
brokermod: ModuleType,
loglevel: str = 'info',
rate: int = 3,
2020-05-23 20:02:15 +00:00
test: str = '',
) -> None:
2018-02-08 07:15:43 +00:00
'''Launch kivy app + all other related tasks.
2018-11-13 23:41:40 +00:00
This is started with cli cmd `piker monitor`.
2018-02-08 07:15:43 +00:00
'''
2019-01-06 00:07:54 +00:00
feed = DataFeed(portal, brokermod)
async with feed.open_stream(
2020-05-23 20:02:15 +00:00
symbols,
'stock',
rate=rate,
test=test,
) as (quote_gen, first_quotes):
first_quotes_list = list(first_quotes.copy().values())
quotes = list(first_quotes.copy().values())
# build out UI
Window.set_title(f"monitor: {name}\t(press ? for help)")
Builder.load_string(_kv)
box = BoxLayout(orientation='vertical', spacing=0)
# define bid-ask "stacked" cells
# (TODO: needs some rethinking and renaming for sure)
bidasks = brokermod._stock_bidasks
# add header row
headers = list(first_quotes_list[0].keys())
headers.remove('displayable')
header = Row(
{key: key for key in headers},
headers=headers,
bidasks=bidasks,
is_header=True,
size_hint=(1, None),
2019-01-15 02:12:35 +00:00
)
box.add_widget(header)
# build table
table = TickerTable(
cols=1,
size_hint=(1, None),
2019-01-15 02:12:35 +00:00
)
for ticker_record in first_quotes_list:
symbol = ticker_record['symbol']
table.append_row(
symbol,
Row(
ticker_record,
headers=('symbol',),
bidasks=bidasks,
no_cell=('displayable',),
table=table
)
)
table.last_clicked_row = next(iter(table.symbols2rows.values()))
# associate the col headers row with the ticker table even though
# they're technically wrapped separately in containing BoxLayout
header.table = table
# mark the initial sorted column header as bold and underlined
sort_cell = header.get_cell(table.sort_key)
sort_cell.bold = sort_cell.underline = True
table.last_clicked_col_cell = sort_cell
# set up a pager view for large ticker lists
table.bind(minimum_height=table.setter('height'))
async def spawn_opts_chain():
"""Spawn an options chain UI in a new subactor.
"""
from .option_chain import _async_main
try:
async with tractor.open_nursery() as tn:
portal = await tn.run_in_actor(
'optschain',
_async_main,
symbol=table.last_clicked_row._last_record['symbol'],
brokername=brokermod.name,
loglevel=tractor.log.get_loglevel(),
)
except tractor.RemoteActorError:
# don't allow option chain errors to crash this monitor
# this is, like, the most basic of resliency policies
log.exception(f"{portal.actor.name} crashed:")
async with trio.open_nursery() as nursery:
pager = PagerView(
container=box,
contained=table,
nursery=nursery,
# spawn an option chain on 'o' keybinding
kbctls={('o',): spawn_opts_chain},
)
box.add_widget(pager)
widgets = {
'root': box,
'table': table,
'box': box,
'header': header,
'pager': pager,
}
global _widgets
_widgets = widgets
nursery.start_soon(
update_quotes,
nursery,
brokermod.format_stock_quote,
widgets,
quote_gen,
feed._symbol_data_cache,
quotes
)
try:
await async_runTouchApp(widgets['root'])
finally:
# cancel remote data feed task
await quote_gen.aclose()
# cancel GUI update task
nursery.cancel_scope.cancel()