Initial dynamic option chain UI draft
There's still a ton to polish (and some bugs to fix) but this is a first working draft of a real-time option chain! Insights and todos: - `kivy` widgets need to be cached and reused (eg. rows, cells, etc.) for speed since it seems creating new ones constantly is quite taxing on the CPU - the chain will tear down and re-setup the option data feed stream each time a different contract expiry button set is clicked - there's still some weird bug with row highlighting where it seems rows added from a new expiry set (which weren't previously rendered) aren't being highlighted reliablykivy_mainline_and_py3.8
parent
1d1be9dd77
commit
9e4786e62f
|
@ -304,6 +304,9 @@ class Row(GridLayout, HoverBehavior):
|
||||||
cell.key = key
|
cell.key = key
|
||||||
self._cell_widgets[key] = cell
|
self._cell_widgets[key] = cell
|
||||||
|
|
||||||
|
def iter_cells(self):
|
||||||
|
return self._cell_widgets.items()
|
||||||
|
|
||||||
def get_cell(self, key):
|
def get_cell(self, key):
|
||||||
return self._cell_widgets.get(key)
|
return self._cell_widgets.get(key)
|
||||||
|
|
||||||
|
@ -356,7 +359,7 @@ class Row(GridLayout, HoverBehavior):
|
||||||
"""Highlight layout on enter.
|
"""Highlight layout on enter.
|
||||||
"""
|
"""
|
||||||
log.debug(
|
log.debug(
|
||||||
f"Entered row {type(self)} through {self.border_point}")
|
f"Entered row {self} through {self.border_point}")
|
||||||
# don't highlight header row
|
# don't highlight header row
|
||||||
if getattr(self, 'is_header', None):
|
if getattr(self, 'is_header', None):
|
||||||
self.hovered = False
|
self.hovered = False
|
||||||
|
@ -365,7 +368,7 @@ class Row(GridLayout, HoverBehavior):
|
||||||
"""Un-highlight layout on exit.
|
"""Un-highlight layout on exit.
|
||||||
"""
|
"""
|
||||||
log.debug(
|
log.debug(
|
||||||
f"Left row {type(self)} through {self.border_point}")
|
f"Left row {self} through {self.border_point}")
|
||||||
|
|
||||||
|
|
||||||
class TickerTable(GridLayout):
|
class TickerTable(GridLayout):
|
||||||
|
@ -424,7 +427,7 @@ class TickerTable(GridLayout):
|
||||||
symbol name. Most naive algo possible for the moment.
|
symbol name. Most naive algo possible for the moment.
|
||||||
"""
|
"""
|
||||||
for symbol, row in self.symbols2rows.items():
|
for symbol, row in self.symbols2rows.items():
|
||||||
if patt in symbol:
|
if patt in symbol:
|
||||||
yield symbol, row
|
yield symbol, row
|
||||||
|
|
||||||
def get_row(self, symbol: str) -> Row:
|
def get_row(self, symbol: str) -> Row:
|
||||||
|
@ -442,10 +445,12 @@ async def update_quotes(
|
||||||
widgets: dict,
|
widgets: dict,
|
||||||
agen: AsyncGeneratorType,
|
agen: AsyncGeneratorType,
|
||||||
symbol_data: dict,
|
symbol_data: dict,
|
||||||
first_quotes: dict
|
first_quotes: dict,
|
||||||
|
task_status: trio._core._run._TaskStatus = trio.TASK_STATUS_IGNORED,
|
||||||
):
|
):
|
||||||
"""Process live quotes by updating ticker rows.
|
"""Process live quotes by updating ticker rows.
|
||||||
"""
|
"""
|
||||||
|
log.debug("Initializing UI update loop")
|
||||||
table = widgets['table']
|
table = widgets['table']
|
||||||
flash_keys = {'low', 'high'}
|
flash_keys = {'low', 'high'}
|
||||||
|
|
||||||
|
@ -521,9 +526,8 @@ async def update_quotes(
|
||||||
color_row(row, record, {})
|
color_row(row, record, {})
|
||||||
cache[sym] = row
|
cache[sym] = row
|
||||||
|
|
||||||
# render all rows once up front
|
log.debug("Finished initializing update loop")
|
||||||
table.render_rows(cache)
|
task_status.started()
|
||||||
|
|
||||||
# real-time cell update loop
|
# real-time cell update loop
|
||||||
async for quotes in agen: # new quotes data only
|
async for quotes in agen: # new quotes data only
|
||||||
for symbol, quote in quotes.items():
|
for symbol, quote in quotes.items():
|
||||||
|
@ -538,6 +542,7 @@ async def update_quotes(
|
||||||
log.debug("Waiting on quotes")
|
log.debug("Waiting on quotes")
|
||||||
|
|
||||||
log.warn("Data feed connection dropped")
|
log.warn("Data feed connection dropped")
|
||||||
|
# XXX: if we're cancelled this should never get called
|
||||||
nursery.cancel_scope.cancel()
|
nursery.cancel_scope.cancel()
|
||||||
|
|
||||||
|
|
||||||
|
@ -617,7 +622,8 @@ async def _async_main(
|
||||||
for ticker_record in first_quotes:
|
for ticker_record in first_quotes:
|
||||||
table.append_row(
|
table.append_row(
|
||||||
ticker_record['symbol'],
|
ticker_record['symbol'],
|
||||||
Row(ticker_record, headers=('symbol',), bidasks=bidasks, table=table)
|
Row(ticker_record, headers=('symbol',),
|
||||||
|
bidasks=bidasks, table=table)
|
||||||
)
|
)
|
||||||
|
|
||||||
# associate the col headers row with the ticker table even though
|
# associate the col headers row with the ticker table even though
|
||||||
|
|
|
@ -5,8 +5,11 @@ Launch with ``piker options <symbol>``.
|
||||||
"""
|
"""
|
||||||
import types
|
import types
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
from typing import Dict, List
|
||||||
|
# import typing
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
|
from async_generator import asynccontextmanager
|
||||||
import tractor
|
import tractor
|
||||||
from kivy.uix.boxlayout import BoxLayout
|
from kivy.uix.boxlayout import BoxLayout
|
||||||
from kivy.lang import Builder
|
from kivy.lang import Builder
|
||||||
|
@ -27,129 +30,328 @@ async def modify_symbol(symbol):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ExpiryButton(HeaderCell):
|
|
||||||
def on_press(self, value=None):
|
|
||||||
log.info(f"Clicked {self}")
|
|
||||||
|
|
||||||
|
|
||||||
class StrikeCell(Cell):
|
class StrikeCell(Cell):
|
||||||
"""Strike cell"""
|
"""Strike cell"""
|
||||||
|
|
||||||
|
|
||||||
_no_display = ['symbol', 'contract_type', 'strike', 'time', 'open']
|
_no_display = ['symbol', 'contract_type', 'strike', 'time', 'open']
|
||||||
|
_strike_row_cache = {}
|
||||||
|
_strike_cell_cache = {}
|
||||||
|
|
||||||
|
|
||||||
class StrikeRow(BoxLayout):
|
class StrikeRow(BoxLayout):
|
||||||
"""A 'row' composed of two ``Row``s sandwiching a
|
"""A 'row' composed of two ``Row``s sandwiching a
|
||||||
``StrikeCell`.
|
``StrikeCell`.
|
||||||
"""
|
"""
|
||||||
|
_row_cache = {}
|
||||||
|
|
||||||
def __init__(self, strike, **kwargs):
|
def __init__(self, strike, **kwargs):
|
||||||
super().__init__(orientation='horizontal', **kwargs)
|
super().__init__(orientation='horizontal', **kwargs)
|
||||||
self.strike = strike
|
self.strike = strike
|
||||||
# store 2 rows: 1 for call, 1 for put
|
# store 2 rows: 1 for call, 1 for put
|
||||||
self._sub_rows = {}
|
self._sub_rows = {}
|
||||||
self.table = None
|
self._widgets_added = False
|
||||||
|
|
||||||
def append_sub_row(
|
def append_sub_row(
|
||||||
self,
|
self,
|
||||||
record: dict,
|
record: dict,
|
||||||
|
displayable: dict,
|
||||||
bidasks=None,
|
bidasks=None,
|
||||||
headers=(),
|
headers=(),
|
||||||
table=None,
|
table=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> None:
|
) -> None:
|
||||||
if self.is_populated():
|
# if self.is_populated():
|
||||||
raise TypeError(f"{self} can only append two sub-rows?")
|
# raise TypeError(f"{self} can only append two sub-rows?")
|
||||||
|
|
||||||
# the 'contract_type' determines whether this
|
# the 'contract_type' determines whether this
|
||||||
# is a put or call row
|
# is a put or call row
|
||||||
contract_type = record['contract_type']
|
contract_type = record['contract_type']
|
||||||
|
|
||||||
# reverse order of call side cells
|
# We want to only create a few ``Row`` widgets as possible to
|
||||||
if contract_type == 'call':
|
# speed up rendering; we cache sub rows after creation.
|
||||||
record = dict(list(reversed(list(record.items()))))
|
row = self._row_cache.get((self.strike, contract_type))
|
||||||
|
if not row:
|
||||||
|
# reverse order of call side cells
|
||||||
|
if contract_type == 'call':
|
||||||
|
record = dict(list(reversed(list(record.items()))))
|
||||||
|
row = Row(
|
||||||
|
record,
|
||||||
|
bidasks=bidasks,
|
||||||
|
headers=headers,
|
||||||
|
table=table,
|
||||||
|
no_cell=_no_display,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
self._row_cache[(self.strike, contract_type)] = row
|
||||||
|
else:
|
||||||
|
# must update the internal cells
|
||||||
|
row.update(record, displayable)
|
||||||
|
|
||||||
row = Row(
|
|
||||||
record,
|
|
||||||
bidasks=bidasks,
|
|
||||||
headers=headers,
|
|
||||||
table=table,
|
|
||||||
no_cell=_no_display,
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
# reassign widget for when rendered in the update loop
|
# reassign widget for when rendered in the update loop
|
||||||
row.widget = self
|
row.widget = self
|
||||||
self._sub_rows[contract_type] = row
|
self._sub_rows[contract_type] = row
|
||||||
if self.is_populated():
|
|
||||||
|
if self.is_populated() and not self._widgets_added:
|
||||||
# calls on the left
|
# calls on the left
|
||||||
self.add_widget(self._sub_rows['call'])
|
self.add_widget(self._sub_rows['call'])
|
||||||
# strikes in the middle
|
strike_cell = _strike_cell_cache.setdefault(
|
||||||
self.add_widget(
|
self.strike, StrikeCell(
|
||||||
StrikeCell(
|
|
||||||
key=self.strike,
|
key=self.strike,
|
||||||
text=str(self.strike),
|
text=str(self.strike),
|
||||||
is_header=True,
|
is_header=True,
|
||||||
# make centre strike cell nice and small
|
# make centre strike cell nice and small
|
||||||
size_hint=(1/8., 1),
|
size_hint=(1/10., 1),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
# strikes in the middle
|
||||||
|
self.add_widget(strike_cell)
|
||||||
# puts on the right
|
# puts on the right
|
||||||
self.add_widget(self._sub_rows['put'])
|
self.add_widget(self._sub_rows['put'])
|
||||||
|
self._widgets_added = True
|
||||||
|
|
||||||
def is_populated(self):
|
def is_populated(self):
|
||||||
"""Bool determing if both a put and call subrow have beed appended.
|
"""Bool determing if both a put and call subrow have beed appended.
|
||||||
"""
|
"""
|
||||||
return len(self._sub_rows) == 2
|
return len(self._sub_rows) == 2
|
||||||
|
|
||||||
|
def has_widgets(self):
|
||||||
|
return self._widgets_added
|
||||||
|
|
||||||
def update(self, record, displayable):
|
def update(self, record, displayable):
|
||||||
self._sub_rows[record['contract_type']].update(
|
self._sub_rows[record['contract_type']].update(
|
||||||
record, displayable)
|
record, displayable)
|
||||||
|
|
||||||
|
|
||||||
async def _async_main(
|
class ExpiryButton(HeaderCell):
|
||||||
symbol: str,
|
def on_press(self, value=None):
|
||||||
|
log.info(f"Clicked {self}")
|
||||||
|
if self.chain.sub[1] == self.key:
|
||||||
|
log.info(f"Clicked {self} is already selected")
|
||||||
|
return
|
||||||
|
log.info(f"Subscribing for {self.chain.sub}")
|
||||||
|
self.chain.start_displaying(self.chain.sub[0], self.key)
|
||||||
|
|
||||||
|
|
||||||
|
class DataFeed(object):
|
||||||
|
"""Data feed client for streaming symbol data from a remote
|
||||||
|
broker data source.
|
||||||
|
"""
|
||||||
|
def __init__(self, portal, brokermod):
|
||||||
|
self.portal = portal
|
||||||
|
self.brokermod = brokermod
|
||||||
|
self.sub = None
|
||||||
|
self.quote_gen = None
|
||||||
|
|
||||||
|
async def open_stream(self, symbols, rate=3, test=None):
|
||||||
|
if self.quote_gen is not None and symbols != self.sub:
|
||||||
|
log.info(f"Stopping existing subscription for {self.sub}")
|
||||||
|
await self.quote_gen.aclose()
|
||||||
|
self.sub = symbols
|
||||||
|
|
||||||
|
if test:
|
||||||
|
# stream from a local test file
|
||||||
|
quote_gen = await self.portal.run(
|
||||||
|
"piker.brokers.data", 'stream_from_file',
|
||||||
|
filename=test
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 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='option',
|
||||||
|
rate=rate,
|
||||||
|
)
|
||||||
|
|
||||||
|
# get first quotes response
|
||||||
|
log.debug(f"Waiting on first quote for {symbols}...")
|
||||||
|
quotes = await quote_gen.__anext__()
|
||||||
|
|
||||||
|
self.quote_gen = quote_gen
|
||||||
|
self.first_quotes = quotes
|
||||||
|
# self.records = records
|
||||||
|
# self.displayables = displayables
|
||||||
|
return quote_gen, quotes
|
||||||
|
|
||||||
|
|
||||||
|
class OptionChain(object):
|
||||||
|
"""A real-time options chain UI.
|
||||||
|
"""
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
symbol: str,
|
||||||
|
expiry: str,
|
||||||
|
widgets: dict,
|
||||||
|
bidasks: Dict[str, List[str]],
|
||||||
|
feed: DataFeed,
|
||||||
|
rate: int = 1,
|
||||||
|
):
|
||||||
|
self.sub = (symbol, expiry)
|
||||||
|
self.widgets = widgets
|
||||||
|
self.bidasks = bidasks
|
||||||
|
self._strikes2rows = {}
|
||||||
|
self._nursery = None
|
||||||
|
self.feed = feed
|
||||||
|
self._update_cs = None
|
||||||
|
# TODO: this should be moved down to the data feed layer
|
||||||
|
# right now it's only needed for the UI uupdate loop to cancel itself
|
||||||
|
self._first_quotes = None
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def open_scope(self):
|
||||||
|
"""Open an internal resource and update task scope required
|
||||||
|
to allow for dynamic real-time operation.
|
||||||
|
"""
|
||||||
|
# assign us to each expiry button
|
||||||
|
for key, button in (
|
||||||
|
self.widgets['expiry_buttons']._cell_widgets.items()
|
||||||
|
):
|
||||||
|
button.chain = self
|
||||||
|
|
||||||
|
async with trio.open_nursery() as n:
|
||||||
|
self._nursery = n
|
||||||
|
n.start_soon(self.start_updating)
|
||||||
|
yield self
|
||||||
|
|
||||||
|
self._nursery = None
|
||||||
|
await self.feed.quote_gen.aclose()
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""Clear the strike rows from the internal table.
|
||||||
|
"""
|
||||||
|
table = self.widgets['table']
|
||||||
|
table.clear_widgets()
|
||||||
|
for strike in self._strikes2rows.copy():
|
||||||
|
self._strikes2rows.pop(strike)
|
||||||
|
|
||||||
|
def render_rows(self, records, displayables):
|
||||||
|
"""Render all strike rows in the internal table.
|
||||||
|
"""
|
||||||
|
log.debug("Rendering rows")
|
||||||
|
table = self.widgets['table']
|
||||||
|
for record, display in zip(
|
||||||
|
sorted(records, key=lambda q: q['strike']),
|
||||||
|
displayables
|
||||||
|
):
|
||||||
|
strike = record['strike']
|
||||||
|
strike_row = _strike_row_cache.setdefault(
|
||||||
|
strike, StrikeRow(strike))
|
||||||
|
strike_row.append_sub_row(
|
||||||
|
record,
|
||||||
|
display,
|
||||||
|
bidasks=self.bidasks,
|
||||||
|
table=table,
|
||||||
|
)
|
||||||
|
if strike_row.is_populated():
|
||||||
|
# We must fill out the the table's symbol2rows manually
|
||||||
|
# using each contracts "symbol" so that the quote updater
|
||||||
|
# task can look up the right row to update easily
|
||||||
|
# See update_quotes() and ``Row`` internals for details.
|
||||||
|
for contract_type, row in strike_row._sub_rows.items():
|
||||||
|
symbol = row._last_record['symbol']
|
||||||
|
table.symbols2rows[symbol] = row
|
||||||
|
|
||||||
|
if strike not in self._strikes2rows:
|
||||||
|
# readding widgets is an error
|
||||||
|
table.add_widget(strike_row)
|
||||||
|
self._strikes2rows[strike] = strike_row
|
||||||
|
|
||||||
|
log.debug("Finished rendering rows!")
|
||||||
|
|
||||||
|
async def start_feed(
|
||||||
|
self,
|
||||||
|
symbol: str,
|
||||||
|
expiry: str,
|
||||||
|
# max QT rate per API customer is approx 4 rps
|
||||||
|
# and usually 3 rps is allocated to the stock monitor
|
||||||
|
rate: int = 1,
|
||||||
|
test: str = None
|
||||||
|
):
|
||||||
|
if self.feed.sub != self.sub:
|
||||||
|
return await self.feed.open_stream([(symbol, expiry)], rate=rate)
|
||||||
|
else:
|
||||||
|
feed = self.feed
|
||||||
|
return feed.quote_gen, feed.first_quotes
|
||||||
|
|
||||||
|
async def start_updating(self):
|
||||||
|
if self._update_cs:
|
||||||
|
self._update_cs.cancel()
|
||||||
|
await trio.sleep(0)
|
||||||
|
|
||||||
|
self.clear()
|
||||||
|
|
||||||
|
if self._nursery is None:
|
||||||
|
raise RuntimeError(
|
||||||
|
"You must call await `start()` first!")
|
||||||
|
|
||||||
|
n = self._nursery
|
||||||
|
log.debug(f"Waiting on first_quotes for {self.sub}")
|
||||||
|
quote_gen, first_quotes = await self.start_feed(*self.sub)
|
||||||
|
log.debug(f"Got first_quotes for {self.sub}")
|
||||||
|
|
||||||
|
# redraw the UI
|
||||||
|
records, displayables = zip(*[
|
||||||
|
self.feed.brokermod.format_option_quote(quote, {})
|
||||||
|
for quote in first_quotes.values()
|
||||||
|
])
|
||||||
|
self.render_rows(records, displayables)
|
||||||
|
|
||||||
|
with trio.open_cancel_scope() as cs:
|
||||||
|
self._update_cs = cs
|
||||||
|
# start quote update loop
|
||||||
|
await n.start(
|
||||||
|
partial(
|
||||||
|
update_quotes,
|
||||||
|
self._nursery,
|
||||||
|
self.feed.brokermod.format_option_quote,
|
||||||
|
self.widgets,
|
||||||
|
quote_gen,
|
||||||
|
symbol_data={},
|
||||||
|
first_quotes=first_quotes,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def start_displaying(self, symbol, expiry):
|
||||||
|
self.sub = (symbol, expiry)
|
||||||
|
self._nursery.start_soon(self.start_updating)
|
||||||
|
|
||||||
|
|
||||||
|
async def new_chain_ui(
|
||||||
portal: tractor._portal.Portal,
|
portal: tractor._portal.Portal,
|
||||||
|
symbol: str,
|
||||||
|
expiry: str,
|
||||||
|
contracts,
|
||||||
brokermod: types.ModuleType,
|
brokermod: types.ModuleType,
|
||||||
rate: int = 4,
|
nursery: trio._core._run.Nursery,
|
||||||
test: bool = False
|
rate: int = 1,
|
||||||
) -> None:
|
) -> None:
|
||||||
'''Launch kivy app + all other related tasks.
|
"""Create and return a new option chain UI.
|
||||||
|
"""
|
||||||
This is started with cli cmd `piker options`.
|
widgets = {}
|
||||||
'''
|
|
||||||
# retreive all contracts
|
|
||||||
all_contracts = await contracts(brokermod, symbol)
|
|
||||||
first_expiry = next(iter(all_contracts)).expiry
|
|
||||||
|
|
||||||
if test:
|
|
||||||
# stream from a local test file
|
|
||||||
quote_gen = await portal.run(
|
|
||||||
"piker.brokers.data", 'stream_from_file',
|
|
||||||
filename=test
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# start live streaming from broker daemon
|
|
||||||
quote_gen = await portal.run(
|
|
||||||
"piker.brokers.data",
|
|
||||||
'start_quote_stream',
|
|
||||||
broker=brokermod.name,
|
|
||||||
symbols=[(symbol, first_expiry)],
|
|
||||||
feed_type='option',
|
|
||||||
)
|
|
||||||
|
|
||||||
# get first quotes response
|
|
||||||
log.debug("Waiting on first quote...")
|
|
||||||
quotes = await quote_gen.__anext__()
|
|
||||||
records, displayables = zip(*[
|
|
||||||
brokermod.format_option_quote(quote, {})
|
|
||||||
for quote in quotes.values()
|
|
||||||
])
|
|
||||||
|
|
||||||
# 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._option_bidasks
|
bidasks = brokermod._option_bidasks
|
||||||
|
|
||||||
# build out UI
|
feed = DataFeed(portal, brokermod)
|
||||||
|
chain = OptionChain(
|
||||||
|
symbol,
|
||||||
|
expiry,
|
||||||
|
widgets,
|
||||||
|
bidasks,
|
||||||
|
feed,
|
||||||
|
rate=rate,
|
||||||
|
)
|
||||||
|
|
||||||
|
quote_gen, first_quotes = await chain.start_feed(symbol, expiry)
|
||||||
|
records, displayables = zip(*[
|
||||||
|
brokermod.format_option_quote(quote, {})
|
||||||
|
for quote in first_quotes.values()
|
||||||
|
])
|
||||||
|
|
||||||
|
# build out root UI
|
||||||
title = f"option chain: {symbol}\t(press ? for help)"
|
title = f"option chain: {symbol}\t(press ? for help)"
|
||||||
Window.set_title(title)
|
Window.set_title(title)
|
||||||
|
|
||||||
|
@ -163,7 +365,7 @@ async def _async_main(
|
||||||
# TODO: figure out how to compact these buttons
|
# TODO: figure out how to compact these buttons
|
||||||
expiries = {
|
expiries = {
|
||||||
key.expiry: key.expiry[:key.expiry.find('T')]
|
key.expiry: key.expiry[:key.expiry.find('T')]
|
||||||
for key in all_contracts
|
for key in contracts
|
||||||
}
|
}
|
||||||
expiry_buttons = Row(
|
expiry_buttons = Row(
|
||||||
record=expiries,
|
record=expiries,
|
||||||
|
@ -182,94 +384,77 @@ async def _async_main(
|
||||||
header_record['contract_type'] = 'put'
|
header_record['contract_type'] = 'put'
|
||||||
header_row.append_sub_row(
|
header_row.append_sub_row(
|
||||||
header_record,
|
header_record,
|
||||||
headers=headers,
|
|
||||||
bidasks=bidasks,
|
|
||||||
is_header=True,
|
|
||||||
size_hint=(1, None),
|
|
||||||
|
|
||||||
)
|
|
||||||
header_record['contract_type'] = 'call'
|
|
||||||
header_row.append_sub_row(
|
|
||||||
header_record,
|
header_record,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
bidasks=bidasks,
|
bidasks=bidasks,
|
||||||
is_header=True,
|
is_header=True,
|
||||||
size_hint=(1, None),
|
size_hint=(1, None),
|
||||||
|
)
|
||||||
|
header_record['contract_type'] = 'call'
|
||||||
|
header_row.append_sub_row(
|
||||||
|
header_record,
|
||||||
|
header_record,
|
||||||
|
headers=headers,
|
||||||
|
bidasks=bidasks,
|
||||||
|
is_header=True,
|
||||||
|
size_hint=(1, None),
|
||||||
)
|
)
|
||||||
container.add_widget(header_row)
|
container.add_widget(header_row)
|
||||||
|
|
||||||
table = TickerTable(
|
table = TickerTable(
|
||||||
sort_key='strike',
|
sort_key='strike',
|
||||||
cols=1,
|
cols=1,
|
||||||
size_hint=(1, None),
|
size_hint=(1, None),
|
||||||
)
|
)
|
||||||
header_row.table = table
|
header_row.table = table
|
||||||
|
table.bind(minimum_height=table.setter('height'))
|
||||||
|
pager = PagerView(
|
||||||
|
container=container,
|
||||||
|
contained=table,
|
||||||
|
nursery=nursery
|
||||||
|
)
|
||||||
|
container.add_widget(pager)
|
||||||
|
widgets.update({
|
||||||
|
'root': container,
|
||||||
|
'container': container,
|
||||||
|
'table': table,
|
||||||
|
'expiry_buttons': expiry_buttons,
|
||||||
|
'pager': pager,
|
||||||
|
})
|
||||||
|
return chain
|
||||||
|
|
||||||
strike_rows = {}
|
|
||||||
for record, display in zip(sorted(
|
|
||||||
records,
|
|
||||||
key=lambda q: q['strike'],
|
|
||||||
), displayables):
|
|
||||||
strike = record['strike']
|
|
||||||
strike_row = strike_rows.setdefault(
|
|
||||||
strike, StrikeRow(strike))
|
|
||||||
strike_row.append_sub_row(
|
|
||||||
record,
|
|
||||||
bidasks=bidasks,
|
|
||||||
table=table,
|
|
||||||
)
|
|
||||||
if strike_row.is_populated():
|
|
||||||
# We must fill out the the table's symbol2rows manually
|
|
||||||
# using each contracts "symbol" so that the quote updater
|
|
||||||
# task can look up the right row to update easily
|
|
||||||
# See update_quotes() and ``Row`` for details.
|
|
||||||
for contract_type, row in strike_row._sub_rows.items():
|
|
||||||
table.symbols2rows[row._last_record['symbol']] = row
|
|
||||||
|
|
||||||
table.append_row(symbol, strike_row)
|
async def _async_main(
|
||||||
|
symbol: str,
|
||||||
|
portal: tractor._portal.Portal,
|
||||||
|
brokermod: types.ModuleType,
|
||||||
|
rate: int = 1,
|
||||||
|
test: bool = False
|
||||||
|
) -> None:
|
||||||
|
'''Launch kivy app + all other related tasks.
|
||||||
|
|
||||||
|
This is started with cli cmd `piker options`.
|
||||||
|
'''
|
||||||
|
# retreive all contracts just because we need a default when the
|
||||||
|
# UI starts up
|
||||||
|
all_contracts = await contracts(brokermod, symbol)
|
||||||
|
# start streaming soonest contract by default
|
||||||
|
first_expiry = next(iter(all_contracts)).expiry
|
||||||
|
|
||||||
async with trio.open_nursery() as nursery:
|
async with trio.open_nursery() as nursery:
|
||||||
# set up a pager view for large ticker lists
|
# set up a pager view for large ticker lists
|
||||||
table.bind(minimum_height=table.setter('height'))
|
chain = await new_chain_ui(
|
||||||
pager = PagerView(
|
portal,
|
||||||
container=container,
|
symbol,
|
||||||
contained=table,
|
first_expiry,
|
||||||
nursery=nursery
|
all_contracts,
|
||||||
|
brokermod,
|
||||||
|
nursery,
|
||||||
|
rate=rate,
|
||||||
)
|
)
|
||||||
container.add_widget(pager)
|
async with chain.open_scope():
|
||||||
widgets = {
|
try:
|
||||||
'root': container,
|
# Trio-kivy entry point.
|
||||||
'container': container,
|
await async_runTouchApp(chain.widgets['root']) # run kivy
|
||||||
'table': table,
|
finally:
|
||||||
'expiry_buttons': expiry_buttons,
|
# cancel GUI update task
|
||||||
'pager': pager,
|
nursery.cancel_scope.cancel()
|
||||||
}
|
|
||||||
nursery.start_soon(
|
|
||||||
partial(
|
|
||||||
update_quotes,
|
|
||||||
nursery,
|
|
||||||
brokermod.format_option_quote,
|
|
||||||
widgets,
|
|
||||||
quote_gen,
|
|
||||||
symbol_data={},
|
|
||||||
first_quotes=quotes,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
# Trio-kivy entry point.
|
|
||||||
await async_runTouchApp(widgets['root']) # run kivy
|
|
||||||
finally:
|
|
||||||
await quote_gen.aclose() # cancel aysnc gen call
|
|
||||||
# un-subscribe from symbols stream (cancel if brokerd
|
|
||||||
# was already torn down - say by SIGINT)
|
|
||||||
with trio.move_on_after(0.2):
|
|
||||||
await portal.run(
|
|
||||||
"piker.brokers.data", 'modify_quote_stream',
|
|
||||||
broker=brokermod.name,
|
|
||||||
feed_type='option',
|
|
||||||
symbols=[]
|
|
||||||
)
|
|
||||||
|
|
||||||
# cancel GUI update task
|
|
||||||
nursery.cancel_scope.cancel()
|
|
||||||
|
|
Loading…
Reference in New Issue