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.
|pypi| |travis| |versions| |license| |docs|
|travis|
``piker`` is an attempt at a pro-grade, broker agnostic, next-gen FOSS toolset for real-time
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):
- Python 3.7+
- ``trio``
- ``tractor``
- trio_
- tractor_
- kivy_
.. |travis| image:: https://img.shields.io/travis/pikers/piker/master.svg
: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
*******
``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.
A couple bleeding edge components are being used atm pertaining to
@ -33,7 +42,20 @@ For a development install::
pipenv install --dev -e .
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

View File

@ -18,7 +18,7 @@ def get_brokermod(brokername: str) -> ModuleType:
"""Return the imported broker module by name.
"""
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]
return module

View File

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

View File

@ -7,6 +7,7 @@ Launch with ``piker monitor <watchlist name>``.
"""
from types import ModuleType, AsyncGeneratorType
from typing import List, Callable
from functools import partial
import trio
import tractor
@ -163,6 +164,7 @@ async def _async_main(
portal: tractor._portal.Portal,
tickers: List[str],
brokermod: ModuleType,
loglevel: str = 'info',
rate: int = 3,
test: bool = False
) -> None:
@ -212,6 +214,7 @@ async def _async_main(
Row(ticker_record, headers=('symbol',),
bidasks=bidasks, 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
@ -226,11 +229,28 @@ async def _async_main(
table.bind(minimum_height=table.setter('height'))
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:
pager = PagerView(
container=box,
contained=table,
nursery=nursery
nursery=nursery,
# spawn an option chain on 'o' keybinding
kbctls={('o',): spawn_opts_chain},
)
box.add_widget(pager)

View File

@ -15,8 +15,9 @@ from kivy.app import async_runTouchApp
from kivy.core.window import Window
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 import get_brokermod
from .pager import PagerView
from .tabular import Row, HeaderCell, Cell, TickerTable
@ -408,7 +409,7 @@ class OptionChain(object):
self.render_rows(records, displayables)
with trio.open_cancel_scope() as cs:
with trio.CancelScope() as cs:
self._update_cs = cs
await self._nursery.start(
partial(
@ -479,28 +480,36 @@ async def new_chain_ui(
async def _async_main(
symbol: str,
portal: tractor._portal.Portal,
brokermod: types.ModuleType,
brokername: str,
rate: int = 1,
loglevel: str = 'info',
test: bool = False
) -> None:
'''Launch kivy app + all other related tasks.
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:
# set up a pager view for large ticker lists
chain = await new_chain_ui(
portal,
symbol,
brokermod,
rate=rate,
)
async with chain.open_rt_display(nursery, symbol):
try:
await async_runTouchApp(chain.widgets['root'])
finally:
if chain._quote_gen:
await chain._quote_gen.aclose()
# cancel GUI update task
nursery.cancel_scope.cancel()
# get a portal to the data feed daemon
async with tractor.wait_for_actor('brokerd') as portal:
# set up a pager view for large ticker lists
chain = await new_chain_ui(
portal,
symbol,
brokermod,
rate=rate,
)
async with chain.open_rt_display(nursery, symbol):
try:
await async_runTouchApp(chain.widgets['root'])
finally:
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 kivy.core.window import Window
from kivy.uix.widget import Widget
from kivy.uix.textinput import TextInput
from kivy.uix.scrollview import ScrollView
@ -12,7 +13,12 @@ from ..log import get_logger
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.
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:
async for kb, keycode, text, modifiers in keyq:
log.debug(
f"Keyboard input received:\n"
f"key {keycode}\ntext {text}\nmodifiers {modifiers}"
)
log.debug(f"""
kb: {kb}
keycode: {keycode}
text: {text}
modifiers: {modifiers}
patts2funcs: {patts2funcs}
""")
code, key = keycode
if modifiers and key in modifiers:
continue
@ -52,8 +61,8 @@ async def handle_input(widget, patts2funcs: dict, patt_len_limit=3):
# stop kb queue to avoid duplicate input processing
keyq.stop()
log.debug(f'invoking kb coro func {func}')
await func()
log.debug(f'spawning task for kb func {func}')
nursery.start_soon(func)
last_patt = []
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}')
func()
last_patt = []
break
if len(last_patt) > patt_len_limit:
last_patt = []
@ -75,26 +85,41 @@ async def handle_input(widget, patts2funcs: dict, patt_len_limit=3):
class SearchBar(TextInput):
def __init__(self, kbctls: dict, container: 'Widget', pager: 'PagerView',
searcher, **kwargs):
def __init__(
self,
container: Widget,
pager: 'PagerView',
searcher,
**kwargs
):
super(SearchBar, self).__init__(
multiline=False,
hint_text='Ticker Search',
cursor_blink=False,
**kwargs
)
self.cursor_blink = False
self.foreground_color = self.hint_text_color # actually readable
self.kbctls = kbctls
self._container = container
self._pager = pager
self._searcher = searcher
# indicate to ``handle_input`` that search is activated on '/'
self.kbctls.update({
self._pager.kbctls.update({
('/',): self.handle_input
})
self.kbctls = {
('ctrl-c',): self.undisplay,
}
self._sugg_template = ' '*4 + '{} matches: '
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):
self.suggestion_text = ''
suffix = self._sugg_template.format(len(matches) or "No")
@ -138,14 +163,31 @@ class SearchBar(TextInput):
self._pager.scroll_to(widget)
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.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
await self.async_bind('on_text_validate').__aiter__().__anext__()
log.debug(f"Seach text is {self.text}")
log.debug("Closing search bar")
self._container.remove_widget(self) # stop displaying
# restore old keyboard bindings
self._pager.kbctls.clear()
self._pager.kbctls.update(old_ctls)
return self.text
@ -167,10 +209,9 @@ class PagerView(ScrollView):
# add contained child widget (can only be one)
self._contained = contained
self.add_widget(contained)
self.search = SearchBar(
self.kbctls, container, self, searcher=contained)
self.search = SearchBar(container, self, searcher=contained)
# 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):
'''Scroll in the y direction [0, 1].

View File

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