Merge pull request #76 from pikers/optschain_as_subactor

Spawn optschain from monitor via keybinding
kivy_mainline_and_py3.8
goodboy 2019-03-21 22:55:49 -04:00 committed by GitHub
commit 8964b7a5fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 139 additions and 44 deletions

View File

@ -2,7 +2,7 @@ piker
----- -----
Trading gear for hackers. Trading gear for hackers.
|pypi| |travis| |versions| |license| |docs| |travis|
``piker`` is an attempt at a pro-grade, broker agnostic, next-gen FOSS toolset for real-time ``piker`` is an attempt at a pro-grade, broker agnostic, next-gen FOSS toolset for real-time
trading and financial analysis. trading and financial analysis.
@ -10,16 +10,25 @@ trading and financial analysis.
It tries to use as much cutting edge tech as possible including (but not limited to): It tries to use as much cutting edge tech as possible including (but not limited to):
- Python 3.7+ - Python 3.7+
- ``trio`` - trio_
- ``tractor`` - tractor_
- kivy_
.. |travis| image:: https://img.shields.io/travis/pikers/piker/master.svg .. |travis| image:: https://img.shields.io/travis/pikers/piker/master.svg
:target: https://travis-ci.org/pikers/piker :target: https://travis-ci.org/pikers/piker
.. _trio: https://github.com/python-trio/trio
.. _tractor: https://github.com/goodboy/tractor
.. _kivy: https://kivy.org
Also, we're always open to new framework suggestions and ideas!
Building the best looking, most reliable, keyboard friendly trading platform is the dream.
Feel free to pipe in with your ideas and quiffs.
Install Install
******* *******
``piker`` is currently under heavy alpha development and as such should ``piker`` is currently under heavy pre-alpha development and as such should
be cloned from this repo and hacked on directly. be cloned from this repo and hacked on directly.
A couple bleeding edge components are being used atm pertaining to A couple bleeding edge components are being used atm pertaining to
@ -33,7 +42,20 @@ For a development install::
pipenv install --dev -e . pipenv install --dev -e .
pipenv shell pipenv shell
To start the real-time index ETF watchlist with the `questrade` backend::
Broker Support
**************
For live data feeds the only fully functional broker at the moment is Questrade_.
Eventual support is in the works for `IB`, `TD Ameritrade` and `IEX`.
If you want your broker supported and they have an API let us know.
.. _Questrade: https://www.questrade.com/api/documentation
Play with some UIs
******************
To start the real-time index monitor with the `questrade` backend::
piker -l info monitor indexes piker -l info monitor indexes

View File

@ -18,7 +18,7 @@ def get_brokermod(brokername: str) -> ModuleType:
"""Return the imported broker module by name. """Return the imported broker module by name.
""" """
module = import_module('.' + brokername, 'piker.brokers') module = import_module('.' + brokername, 'piker.brokers')
# we only allows monkeys because it's for internal keying # we only allow monkeying because it's for internal keying
module.name = module.__name__.split('.')[-1] module.name = module.__name__.split('.')[-1]
return module return module

View File

@ -385,7 +385,7 @@ def optschain(config, symbol, date, tl, rate, test):
""" """
# global opts # global opts
loglevel = config['loglevel'] loglevel = config['loglevel']
brokermod = config['brokermod'] brokername = config['broker']
from .ui.option_chain import _async_main from .ui.option_chain import _async_main
@ -395,9 +395,10 @@ def optschain(config, symbol, date, tl, rate, test):
) as portal: ) as portal:
# run app "main" # run app "main"
await _async_main( await _async_main(
symbol, portal, symbol,
brokermod, brokername,
rate=rate, rate=rate,
loglevel=loglevel,
test=test, test=test,
) )

View File

@ -7,6 +7,7 @@ Launch with ``piker monitor <watchlist name>``.
""" """
from types import ModuleType, AsyncGeneratorType from types import ModuleType, AsyncGeneratorType
from typing import List, Callable from typing import List, Callable
from functools import partial
import trio import trio
import tractor import tractor
@ -163,6 +164,7 @@ async def _async_main(
portal: tractor._portal.Portal, portal: tractor._portal.Portal,
tickers: List[str], tickers: List[str],
brokermod: ModuleType, brokermod: ModuleType,
loglevel: str = 'info',
rate: int = 3, rate: int = 3,
test: bool = False test: bool = False
) -> None: ) -> None:
@ -212,6 +214,7 @@ async def _async_main(
Row(ticker_record, headers=('symbol',), Row(ticker_record, headers=('symbol',),
bidasks=bidasks, table=table) bidasks=bidasks, table=table)
) )
table.last_clicked_row = next(iter(table.symbols2rows.values()))
# associate the col headers row with the ticker table even though # associate the col headers row with the ticker table even though
# they're technically wrapped separately in containing BoxLayout # they're technically wrapped separately in containing BoxLayout
@ -226,11 +229,28 @@ async def _async_main(
table.bind(minimum_height=table.setter('height')) table.bind(minimum_height=table.setter('height'))
ss = tractor.current_actor().statespace ss = tractor.current_actor().statespace
async def spawn_opts_chain():
"""Spawn an options chain UI in a new subactor.
"""
from .option_chain import _async_main
async with tractor.open_nursery() as tn:
await tn.run_in_actor(
'optschain',
_async_main,
symbol=table.last_clicked_row._last_record['symbol'],
brokername=brokermod.name,
# loglevel=tractor.log.get_loglevel(),
)
async with trio.open_nursery() as nursery: async with trio.open_nursery() as nursery:
pager = PagerView( pager = PagerView(
container=box, container=box,
contained=table, contained=table,
nursery=nursery nursery=nursery,
# spawn an option chain on 'o' keybinding
kbctls={('o',): spawn_opts_chain},
) )
box.add_widget(pager) box.add_widget(pager)

View File

@ -15,8 +15,9 @@ from kivy.app import async_runTouchApp
from kivy.core.window import Window from kivy.core.window import Window
from kivy.uix.label import Label from kivy.uix.label import Label
from ..log import get_logger from ..log import get_logger, get_console_log
from ..brokers.data import DataFeed from ..brokers.data import DataFeed
from ..brokers import get_brokermod
from .pager import PagerView from .pager import PagerView
from .tabular import Row, HeaderCell, Cell, TickerTable from .tabular import Row, HeaderCell, Cell, TickerTable
@ -408,7 +409,7 @@ class OptionChain(object):
self.render_rows(records, displayables) self.render_rows(records, displayables)
with trio.open_cancel_scope() as cs: with trio.CancelScope() as cs:
self._update_cs = cs self._update_cs = cs
await self._nursery.start( await self._nursery.start(
partial( partial(
@ -479,28 +480,36 @@ async def new_chain_ui(
async def _async_main( async def _async_main(
symbol: str, symbol: str,
portal: tractor._portal.Portal, brokername: str,
brokermod: types.ModuleType,
rate: int = 1, rate: int = 1,
loglevel: str = 'info',
test: bool = False test: bool = False
) -> None: ) -> None:
'''Launch kivy app + all other related tasks. '''Launch kivy app + all other related tasks.
This is started with cli cmd `piker options`. This is started with cli cmd `piker options`.
''' '''
if loglevel is not None:
get_console_log(loglevel)
brokermod = get_brokermod(brokername)
async with trio.open_nursery() as nursery: async with trio.open_nursery() as nursery:
# set up a pager view for large ticker lists # get a portal to the data feed daemon
chain = await new_chain_ui( async with tractor.wait_for_actor('brokerd') as portal:
portal,
symbol, # set up a pager view for large ticker lists
brokermod, chain = await new_chain_ui(
rate=rate, portal,
) symbol,
async with chain.open_rt_display(nursery, symbol): brokermod,
try: rate=rate,
await async_runTouchApp(chain.widgets['root']) )
finally: async with chain.open_rt_display(nursery, symbol):
if chain._quote_gen: try:
await chain._quote_gen.aclose() await async_runTouchApp(chain.widgets['root'])
# cancel GUI update task finally:
nursery.cancel_scope.cancel() if chain._quote_gen:
await chain._quote_gen.aclose()
# cancel GUI update task
nursery.cancel_scope.cancel()

View File

@ -5,6 +5,7 @@ import inspect
from functools import partial from functools import partial
from kivy.core.window import Window from kivy.core.window import Window
from kivy.uix.widget import Widget
from kivy.uix.textinput import TextInput from kivy.uix.textinput import TextInput
from kivy.uix.scrollview import ScrollView from kivy.uix.scrollview import ScrollView
@ -12,7 +13,12 @@ from ..log import get_logger
log = get_logger('keyboard') log = get_logger('keyboard')
async def handle_input(widget, patts2funcs: dict, patt_len_limit=3): async def handle_input(
nursery,
widget,
patts2funcs: dict,
patt_len_limit=3
) -> None:
"""Handle keyboard input. """Handle keyboard input.
For each character pattern-tuple in ``patts2funcs`` invoke the For each character pattern-tuple in ``patts2funcs`` invoke the
@ -31,10 +37,13 @@ async def handle_input(widget, patts2funcs: dict, patt_len_limit=3):
while True: while True:
async for kb, keycode, text, modifiers in keyq: async for kb, keycode, text, modifiers in keyq:
log.debug( log.debug(f"""
f"Keyboard input received:\n" kb: {kb}
f"key {keycode}\ntext {text}\nmodifiers {modifiers}" keycode: {keycode}
) text: {text}
modifiers: {modifiers}
patts2funcs: {patts2funcs}
""")
code, key = keycode code, key = keycode
if modifiers and key in modifiers: if modifiers and key in modifiers:
continue continue
@ -52,8 +61,8 @@ async def handle_input(widget, patts2funcs: dict, patt_len_limit=3):
# stop kb queue to avoid duplicate input processing # stop kb queue to avoid duplicate input processing
keyq.stop() keyq.stop()
log.debug(f'invoking kb coro func {func}') log.debug(f'spawning task for kb func {func}')
await func() nursery.start_soon(func)
last_patt = [] last_patt = []
break # trigger loop restart break # trigger loop restart
@ -61,6 +70,7 @@ async def handle_input(widget, patts2funcs: dict, patt_len_limit=3):
log.debug(f'invoking kb func {func}') log.debug(f'invoking kb func {func}')
func() func()
last_patt = [] last_patt = []
break
if len(last_patt) > patt_len_limit: if len(last_patt) > patt_len_limit:
last_patt = [] last_patt = []
@ -75,26 +85,41 @@ async def handle_input(widget, patts2funcs: dict, patt_len_limit=3):
class SearchBar(TextInput): class SearchBar(TextInput):
def __init__(self, kbctls: dict, container: 'Widget', pager: 'PagerView', def __init__(
searcher, **kwargs): self,
container: Widget,
pager: 'PagerView',
searcher,
**kwargs
):
super(SearchBar, self).__init__( super(SearchBar, self).__init__(
multiline=False, multiline=False,
hint_text='Ticker Search', hint_text='Ticker Search',
cursor_blink=False,
**kwargs **kwargs
) )
self.cursor_blink = False
self.foreground_color = self.hint_text_color # actually readable self.foreground_color = self.hint_text_color # actually readable
self.kbctls = kbctls
self._container = container self._container = container
self._pager = pager self._pager = pager
self._searcher = searcher self._searcher = searcher
# indicate to ``handle_input`` that search is activated on '/' # indicate to ``handle_input`` that search is activated on '/'
self.kbctls.update({ self._pager.kbctls.update({
('/',): self.handle_input ('/',): self.handle_input
}) })
self.kbctls = {
('ctrl-c',): self.undisplay,
}
self._sugg_template = ' '*4 + '{} matches: ' self._sugg_template = ' '*4 + '{} matches: '
self._matched = [] self._matched = []
def undisplay(self):
"Stop displaying this text widget"
self.dispatch('on_text_validate') # same as pressing <enter>
if self.text_validate_unfocus:
self.focus = False
def suggest(self, matches): def suggest(self, matches):
self.suggestion_text = '' self.suggestion_text = ''
suffix = self._sugg_template.format(len(matches) or "No") suffix = self._sugg_template.format(len(matches) or "No")
@ -138,14 +163,31 @@ class SearchBar(TextInput):
self._pager.scroll_to(widget) self._pager.scroll_to(widget)
async def handle_input(self): async def handle_input(self):
# TODO: wrap this in a cntx mng
old_ctls = self._pager.kbctls.copy()
self._pager.kbctls.clear()
# makes a copy
self._pager.kbctls.update(self.kbctls)
self._container.add_widget(self) # display it self._container.add_widget(self) # display it
self.focus = True # focus immediately (doesn't work from __init__) self.focus = True # focus immediately (doesn't work from __init__)
# select any existing text making the widget ready
# to accept new input right away
if self.text:
self.select_all()
# wait for <enter> to close search bar # wait for <enter> to close search bar
await self.async_bind('on_text_validate').__aiter__().__anext__() await self.async_bind('on_text_validate').__aiter__().__anext__()
log.debug(f"Seach text is {self.text}") log.debug(f"Seach text is {self.text}")
log.debug("Closing search bar") log.debug("Closing search bar")
self._container.remove_widget(self) # stop displaying self._container.remove_widget(self) # stop displaying
# restore old keyboard bindings
self._pager.kbctls.clear()
self._pager.kbctls.update(old_ctls)
return self.text return self.text
@ -167,10 +209,9 @@ class PagerView(ScrollView):
# add contained child widget (can only be one) # add contained child widget (can only be one)
self._contained = contained self._contained = contained
self.add_widget(contained) self.add_widget(contained)
self.search = SearchBar( self.search = SearchBar(container, self, searcher=contained)
self.kbctls, container, self, searcher=contained)
# spawn kb handler task # spawn kb handler task
nursery.start_soon(handle_input, self, self.kbctls) nursery.start_soon(handle_input, nursery, self, self.kbctls)
def move_y(self, val): def move_y(self, val):
'''Scroll in the y direction [0, 1]. '''Scroll in the y direction [0, 1].

View File

@ -383,6 +383,7 @@ class Row(HoverBehavior, GridLayout):
def on_press(self, value=None): def on_press(self, value=None):
log.info(f"Pressed row for {self._last_record['symbol']}") log.info(f"Pressed row for {self._last_record['symbol']}")
if self.table and not self.is_header: if self.table and not self.is_header:
self.table.last_clicked_row = self
for sendchan in self.table._click_queues: for sendchan in self.table._click_queues:
sendchan.send_nowait(self._last_record['symbol']) sendchan.send_nowait(self._last_record['symbol'])
@ -396,6 +397,7 @@ class TickerTable(GridLayout):
self.sort_key = sort_key self.sort_key = sort_key
# for tracking last clicked column header cell # for tracking last clicked column header cell
self.last_clicked_col_cell = None self.last_clicked_col_cell = None
self.last_clicked_row = None
self._auto_sort = auto_sort self._auto_sort = auto_sort
self._symbols2index = {} self._symbols2index = {}
self._sorted = [] self._sorted = []