Port kivy monitor to new tractor stream api

tractor_open_stream_from
Tyler Goodlet 2021-04-29 08:36:55 -04:00
parent a89da98141
commit 3375735914
2 changed files with 147 additions and 153 deletions

View File

@ -20,7 +20,6 @@ Real-time data feed machinery
import time import time
from functools import partial from functools import partial
from dataclasses import dataclass, field from dataclasses import dataclass, field
from itertools import cycle
import socket import socket
import json import json
from types import ModuleType from types import ModuleType
@ -31,7 +30,6 @@ from typing import (
Sequence Sequence
) )
import contextlib import contextlib
from operator import itemgetter
import trio import trio
import tractor import tractor
@ -182,6 +180,8 @@ async def symbol_data(broker: str, tickers: List[str]):
_feeds_cache = {} _feeds_cache = {}
# TODO: use the version of this from .api ?
@asynccontextmanager @asynccontextmanager
async def get_cached_feed( async def get_cached_feed(
brokername: str, brokername: str,
@ -326,6 +326,7 @@ class DataFeed:
self.quote_gen = None self.quote_gen = None
self._symbol_data_cache: Dict[str, Any] = {} self._symbol_data_cache: Dict[str, Any] = {}
@asynccontextmanager
async def open_stream( async def open_stream(
self, self,
symbols: Sequence[str], symbols: Sequence[str],
@ -351,40 +352,32 @@ class DataFeed:
# subscribe for tickers (this performs a possible filtering # subscribe for tickers (this performs a possible filtering
# where invalid symbols are discarded) # where invalid symbols are discarded)
sd = await self.portal.run( sd = await self.portal.run(
"piker.brokers.data", symbol_data,
'symbol_data',
broker=self.brokermod.name, broker=self.brokermod.name,
tickers=symbols tickers=symbols
) )
self._symbol_data_cache.update(sd) self._symbol_data_cache.update(sd)
if test: log.info(f"Starting new stream for {symbols}")
# stream from a local test file
quote_gen = await self.portal.run(
"piker.brokers.data",
'stream_from_file',
filename=test,
)
else:
log.info(f"Starting new stream for {symbols}")
# start live streaming from broker daemon
quote_gen = await self.portal.run(
"piker.brokers.data",
'start_quote_stream',
broker=self.brokermod.name,
symbols=symbols,
feed_type=feed_type,
rate=rate,
)
# get first quotes response # start live streaming from broker daemon
log.debug(f"Waiting on first quote for {symbols}...") async with self.portal.open_stream_from(
quotes = {} start_quote_stream,
quotes = await quote_gen.__anext__() broker=self.brokermod.name,
symbols=symbols,
feed_type=feed_type,
rate=rate,
) as quote_gen:
# get first quotes response
log.debug(f"Waiting on first quote for {symbols}...")
quotes = {}
quotes = await quote_gen.__anext__()
self.quote_gen = quote_gen
self.first_quotes = quotes
yield quote_gen, quotes
self.quote_gen = quote_gen
self.first_quotes = quotes
return quote_gen, quotes
except Exception: except Exception:
if self.quote_gen: if self.quote_gen:
await self.quote_gen.aclose() await self.quote_gen.aclose()
@ -406,8 +399,7 @@ class DataFeed:
"""Call a broker ``Client`` method using RPC and return result. """Call a broker ``Client`` method using RPC and return result.
""" """
return await self.portal.run( return await self.portal.run(
'piker.brokers.data', call_client,
'call_client',
broker=self.brokermod.name, broker=self.brokermod.name,
methname=method, methname=method,
**kwargs **kwargs
@ -425,27 +417,29 @@ async def stream_to_file(
"""Record client side received quotes to file ``filename``. """Record client side received quotes to file ``filename``.
""" """
# an async generator instance # an async generator instance
agen = await portal.run( async with portal.open_stream_from(
"piker.brokers.data", 'start_quote_stream', start_quote_stream,
broker=brokermod.name, symbols=tickers) broker=brokermod.name,
symbols=tickers
) as agen:
fname = filename or f'{watchlist_name}.jsonstream' fname = filename or f'{watchlist_name}.jsonstream'
with open(fname, 'a') as f: with open(fname, 'a') as f:
async for quotes in agen: async for quotes in agen:
f.write(json.dumps(quotes)) f.write(json.dumps(quotes))
f.write('\n--\n') f.write('\n--\n')
return fname return fname
async def stream_from_file( # async def stream_from_file(
filename: str, # filename: str,
): # ):
with open(filename, 'r') as quotes_file: # with open(filename, 'r') as quotes_file:
content = quotes_file.read() # content = quotes_file.read()
pkts = content.split('--')[:-1] # simulate 2 separate quote packets # pkts = content.split('--')[:-1] # simulate 2 separate quote packets
payloads = [json.loads(pkt) for pkt in pkts] # payloads = [json.loads(pkt) for pkt in pkts]
for payload in cycle(payloads): # for payload in cycle(payloads):
yield payload # yield payload
await trio.sleep(0.3) # await trio.sleep(0.3)

View File

@ -179,121 +179,121 @@ async def _async_main(
This is started with cli cmd `piker monitor`. This is started with cli cmd `piker monitor`.
''' '''
feed = DataFeed(portal, brokermod) feed = DataFeed(portal, brokermod)
quote_gen, first_quotes = await feed.open_stream( async with feed.open_stream(
symbols, symbols,
'stock', 'stock',
rate=rate, rate=rate,
test=test, test=test,
) ) as (quote_gen, first_quotes):
first_quotes_list = list(first_quotes.copy().values()) first_quotes_list = list(first_quotes.copy().values())
quotes = list(first_quotes.copy().values()) quotes = list(first_quotes.copy().values())
# build out UI # build out UI
Window.set_title(f"monitor: {name}\t(press ? for help)") Window.set_title(f"monitor: {name}\t(press ? for help)")
Builder.load_string(_kv) Builder.load_string(_kv)
box = BoxLayout(orientation='vertical', spacing=0) box = BoxLayout(orientation='vertical', spacing=0)
# define bid-ask "stacked" cells # define bid-ask "stacked" cells
# (TODO: needs some rethinking and renaming for sure) # (TODO: needs some rethinking and renaming for sure)
bidasks = brokermod._stock_bidasks bidasks = brokermod._stock_bidasks
# add header row # add header row
headers = list(first_quotes_list[0].keys()) headers = list(first_quotes_list[0].keys())
headers.remove('displayable') headers.remove('displayable')
header = Row( header = Row(
{key: key for key in headers}, {key: key for key in headers},
headers=headers, headers=headers,
bidasks=bidasks, bidasks=bidasks,
is_header=True, is_header=True,
size_hint=(1, None), size_hint=(1, None),
)
box.add_widget(header)
# build table
table = TickerTable(
cols=1,
size_hint=(1, None),
)
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())) box.add_widget(header)
# associate the col headers row with the ticker table even though # build table
# they're technically wrapped separately in containing BoxLayout table = TickerTable(
header.table = table cols=1,
size_hint=(1, None),
# mark the initial sorted column header as bold and underlined )
sort_cell = header.get_cell(table.sort_key) for ticker_record in first_quotes_list:
sort_cell.bold = sort_cell.underline = True symbol = ticker_record['symbol']
table.last_clicked_col_cell = sort_cell table.append_row(
symbol,
# set up a pager view for large ticker lists Row(
table.bind(minimum_height=table.setter('height')) ticker_record,
headers=('symbol',),
async def spawn_opts_chain(): bidasks=bidasks,
"""Spawn an options chain UI in a new subactor. no_cell=('displayable',),
""" table=table
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 table.last_clicked_row = next(iter(table.symbols2rows.values()))
# this is, like, the most basic of resliency policies
log.exception(f"{portal.actor.name} crashed:")
async with trio.open_nursery() as nursery: # associate the col headers row with the ticker table even though
pager = PagerView( # they're technically wrapped separately in containing BoxLayout
container=box, header.table = table
contained=table,
nursery=nursery,
# spawn an option chain on 'o' keybinding
kbctls={('o',): spawn_opts_chain},
)
box.add_widget(pager)
widgets = { # mark the initial sorted column header as bold and underlined
'root': box, sort_cell = header.get_cell(table.sort_key)
'table': table, sort_cell.bold = sort_cell.underline = True
'box': box, table.last_clicked_col_cell = sort_cell
'header': header,
'pager': pager,
}
global _widgets # set up a pager view for large ticker lists
_widgets = widgets table.bind(minimum_height=table.setter('height'))
nursery.start_soon( async def spawn_opts_chain():
update_quotes, """Spawn an options chain UI in a new subactor.
nursery, """
brokermod.format_stock_quote, from .option_chain import _async_main
widgets,
quote_gen, try:
feed._symbol_data_cache, async with tractor.open_nursery() as tn:
quotes portal = await tn.run_in_actor(
) 'optschain',
try: _async_main,
await async_runTouchApp(widgets['root']) symbol=table.last_clicked_row._last_record['symbol'],
finally: brokername=brokermod.name,
# cancel remote data feed task loglevel=tractor.log.get_loglevel(),
await quote_gen.aclose() )
# cancel GUI update task except tractor.RemoteActorError:
nursery.cancel_scope.cancel() # 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()