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.
|
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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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].
|
||||||
|
|
|
@ -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 = []
|
||||||
|
|
Loading…
Reference in New Issue