Merge pull request #76 from pikers/optschain_as_subactor
Spawn optschain from monitor via keybindingkivy_mainline_and_py3.8
commit
8964b7a5fb
32
README.rst
32
README.rst
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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].
|
||||
|
|
|
@ -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 = []
|
||||
|
|
Loading…
Reference in New Issue