Port kivy monitor to new tractor stream api
parent
a89da98141
commit
3375735914
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue