2018-02-08 07:15:43 +00:00
|
|
|
"""
|
2018-08-24 03:12:39 +00:00
|
|
|
monitor: a real-time, sorted watchlist.
|
2018-02-09 08:29:10 +00:00
|
|
|
|
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-02-10 03:03:37 +00:00
|
|
|
"""
|
2018-06-27 15:50:02 +00:00
|
|
|
from types import ModuleType, AsyncGeneratorType
|
2018-12-19 01:28:26 +00:00
|
|
|
from typing import List, Callable
|
2018-02-14 07:07:42 +00:00
|
|
|
|
2018-02-08 07:15:43 +00:00
|
|
|
import trio
|
2018-11-23 15:50:40 +00:00
|
|
|
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
|
|
|
|
2020-06-17 18:51:29 +00:00
|
|
|
from ...brokers.data import DataFeed
|
2019-01-03 02:12:42 +00:00
|
|
|
from .tabular import (
|
|
|
|
|
Row, TickerTable, _kv, _black_rgba, colorcode,
|
|
|
|
|
)
|
2020-06-17 18:51:29 +00:00
|
|
|
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-12-02 00:01:36 +00:00
|
|
|
|
2018-08-24 03:12:39 +00:00
|
|
|
log = get_logger('monitor')
|
2018-02-09 07:44:43 +00:00
|
|
|
|
|
|
|
|
|
2018-02-09 00:30:09 +00:00
|
|
|
async def update_quotes(
|
2018-11-23 15:50:40 +00:00
|
|
|
nursery: trio._core._run.Nursery,
|
2018-12-10 06:51:49 +00:00
|
|
|
formatter: Callable,
|
2018-02-09 00:30:09 +00:00
|
|
|
widgets: dict,
|
2018-06-27 15:50:02 +00:00
|
|
|
agen: AsyncGeneratorType,
|
2018-02-09 00:30:09 +00:00
|
|
|
symbol_data: dict,
|
2018-12-13 18:04:05 +00:00
|
|
|
first_quotes: dict,
|
|
|
|
|
task_status: trio._core._run._TaskStatus = trio.TASK_STATUS_IGNORED,
|
2018-02-09 00:30:09 +00:00
|
|
|
):
|
2018-02-08 07:15:43 +00:00
|
|
|
"""Process live quotes by updating ticker rows.
|
|
|
|
|
"""
|
2018-12-13 18:04:05 +00:00
|
|
|
log.debug("Initializing UI update loop")
|
2018-11-23 15:50:40 +00:00
|
|
|
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
|
|
|
|
2018-12-10 06:51:49 +00:00
|
|
|
def color_row(row, record, cells):
|
2018-02-14 07:07:42 +00:00
|
|
|
hdrcell = row.get_cell('symbol')
|
|
|
|
|
chngcell = row.get_cell('%')
|
2018-11-23 15:50:40 +00:00
|
|
|
|
|
|
|
|
# determine daily change color
|
2018-12-10 06:51:49 +00:00
|
|
|
percent_change = record.get('%')
|
2020-06-01 02:59:37 +00:00
|
|
|
if percent_change is not None and percent_change != chngcell:
|
|
|
|
|
daychange = float(percent_change)
|
2018-12-10 06:51:49 +00:00
|
|
|
if daychange < 0.:
|
|
|
|
|
color = colorcode('red2')
|
|
|
|
|
elif daychange > 0.:
|
|
|
|
|
color = colorcode('forestgreen')
|
2020-06-01 02:59:37 +00:00
|
|
|
else:
|
|
|
|
|
color = colorcode('gray')
|
2018-02-09 00:30:09 +00:00
|
|
|
|
2018-12-10 06:51:49 +00:00
|
|
|
# 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:
|
2018-12-10 06:51:49 +00:00
|
|
|
hdrcell.background_color = color
|
2018-02-10 03:03:37 +00:00
|
|
|
|
2020-06-01 02:59:37 +00:00
|
|
|
# update row header and '%' cell text color
|
|
|
|
|
chngcell.color = color
|
|
|
|
|
hdrcell.color = color
|
|
|
|
|
|
2018-11-23 15:50:40 +00:00
|
|
|
# briefly highlight bg of certain cells on each trade execution
|
|
|
|
|
unflash = set()
|
|
|
|
|
tick_color = None
|
|
|
|
|
last = cells.get('last')
|
|
|
|
|
if not last:
|
2020-05-26 18:10:41 +00:00
|
|
|
vol = cells.get('volume')
|
2018-11-23 15:50:40 +00:00
|
|
|
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)
|
|
|
|
|
|
2018-02-09 00:30:09 +00:00
|
|
|
# initial coloring
|
2018-12-15 21:28:28 +00:00
|
|
|
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, {})
|
2018-12-15 21:28:28 +00:00
|
|
|
to_sort.add(row.widget)
|
|
|
|
|
|
|
|
|
|
table.render_rows(to_sort)
|
2018-02-09 00:30:09 +00:00
|
|
|
|
2018-12-13 18:04:05 +00:00
|
|
|
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
|
2018-12-15 21:28:28 +00:00
|
|
|
to_sort = set()
|
2018-03-29 17:02:03 +00:00
|
|
|
for symbol, quote in quotes.items():
|
2018-12-19 01:28:26 +00:00
|
|
|
row = table.get_row(symbol)
|
2018-02-09 00:30:09 +00:00
|
|
|
|
2020-06-01 02:59:37 +00:00
|
|
|
# don't red/green the header cell in ``row.update()``
|
2020-08-10 19:48:57 +00:00
|
|
|
quote.pop('symbol')
|
|
|
|
|
quote.pop('key')
|
2020-06-01 02:59:37 +00:00
|
|
|
|
2018-12-19 01:28:26 +00:00
|
|
|
# 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)
|
2018-12-19 01:28:26 +00:00
|
|
|
if new != last:
|
2018-12-15 21:28:28 +00:00
|
|
|
to_sort.add(row.widget)
|
|
|
|
|
|
2018-12-19 01:28:26 +00:00
|
|
|
# update and color
|
2020-08-10 19:48:57 +00:00
|
|
|
cells = row.update(quote)
|
|
|
|
|
color_row(row, quote, cells)
|
2018-12-19 01:28:26 +00:00
|
|
|
|
2018-12-15 21:28:28 +00:00
|
|
|
if to_sort:
|
|
|
|
|
table.render_rows(to_sort)
|
|
|
|
|
|
2018-04-17 21:19:22 +00:00
|
|
|
log.debug("Waiting on quotes")
|
2018-02-08 07:15:43 +00:00
|
|
|
|
2018-11-23 15:50:40 +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 = {}
|
|
|
|
|
|
|
|
|
|
|
2018-12-30 19:59:54 +00:00
|
|
|
async def stream_symbol_selection():
|
|
|
|
|
"""An RPC async gen for streaming the symbol corresponding
|
|
|
|
|
value corresponding to the last clicked row.
|
2019-03-24 16:09:17 +00:00
|
|
|
|
|
|
|
|
Essentially of an event stream of clicked symbol values.
|
2018-12-30 19:59:54 +00:00
|
|
|
"""
|
2021-02-21 17:32:40 +00:00
|
|
|
global _widgets
|
|
|
|
|
table = _widgets['table']
|
2019-02-22 04:09:19 +00:00
|
|
|
send_chan, recv_chan = trio.open_memory_channel(0)
|
|
|
|
|
table._click_queues.append(send_chan)
|
2018-12-30 19:59:54 +00:00
|
|
|
try:
|
2019-02-22 04:09:19 +00:00
|
|
|
async with recv_chan:
|
|
|
|
|
async for symbol in recv_chan:
|
|
|
|
|
yield symbol
|
2018-12-30 19:59:54 +00:00
|
|
|
finally:
|
2019-02-22 04:09:19 +00:00
|
|
|
table._click_queues.remove(send_chan)
|
2018-12-30 19:59:54 +00:00
|
|
|
|
|
|
|
|
|
2018-11-23 15:50:40 +00:00
|
|
|
async def _async_main(
|
|
|
|
|
name: str,
|
2026-03-24 03:55:07 +00:00
|
|
|
portal: tractor.Portal,
|
2020-05-23 20:02:15 +00:00
|
|
|
symbols: List[str],
|
2018-11-23 15:50:40 +00:00
|
|
|
brokermod: ModuleType,
|
2019-03-22 02:15:08 +00:00
|
|
|
loglevel: str = 'info',
|
2018-12-11 20:22:34 +00:00
|
|
|
rate: int = 3,
|
2020-05-23 20:02:15 +00:00
|
|
|
test: str = '',
|
2018-11-23 15:50:40 +00:00
|
|
|
) -> None:
|
2018-02-08 07:15:43 +00:00
|
|
|
'''Launch kivy app + all other related tasks.
|
2018-02-11 00:54:09 +00:00
|
|
|
|
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)
|
2021-04-29 12:36:55 +00:00
|
|
|
async with feed.open_stream(
|
2020-05-23 20:02:15 +00:00
|
|
|
symbols,
|
|
|
|
|
'stock',
|
|
|
|
|
rate=rate,
|
|
|
|
|
test=test,
|
2021-04-29 12:36:55 +00:00
|
|
|
) 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
|
|
|
)
|
2021-04-29 12:36:55 +00:00
|
|
|
box.add_widget(header)
|
|
|
|
|
|
|
|
|
|
# build table
|
|
|
|
|
table = TickerTable(
|
|
|
|
|
cols=1,
|
|
|
|
|
size_hint=(1, None),
|
2019-01-15 02:12:35 +00:00
|
|
|
)
|
2021-04-29 12:36:55 +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()
|