commit
6b38f25430
|
@ -1,3 +1,23 @@
|
||||||
"""
|
"""
|
||||||
Broker clients, daemons and general back end machinery.
|
Broker clients, daemons and general back end machinery.
|
||||||
"""
|
"""
|
||||||
|
from importlib import import_module
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
|
__brokers__ = [
|
||||||
|
'questrade',
|
||||||
|
'robinhood',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_brokermod(brokername: str) -> ModuleType:
|
||||||
|
"""Return the imported broker module by name.
|
||||||
|
"""
|
||||||
|
return import_module('.' + brokername, 'piker.brokers')
|
||||||
|
|
||||||
|
|
||||||
|
def iter_brokermods():
|
||||||
|
"""Iterate all built-in broker modules.
|
||||||
|
"""
|
||||||
|
for name in __brokers__:
|
||||||
|
yield get_brokermod(name)
|
||||||
|
|
|
@ -8,7 +8,6 @@ from typing import AsyncContextManager
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
from .questrade import QuestradeError
|
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
log = get_logger('broker.core')
|
log = get_logger('broker.core')
|
||||||
|
|
||||||
|
@ -100,8 +99,8 @@ async def poll_tickers(
|
||||||
delay = sleeptime - tot
|
delay = sleeptime - tot
|
||||||
if delay <= 0:
|
if delay <= 0:
|
||||||
log.warn(
|
log.warn(
|
||||||
f"Took {req_time} (request) + {proc_time} (processing) = {tot}"
|
f"Took {req_time} (request) + {proc_time} (processing) "
|
||||||
f" secs (> {sleeptime}) for processing quotes?")
|
f"= {tot} secs (> {sleeptime}) for processing quotes?")
|
||||||
else:
|
else:
|
||||||
log.debug(f"Sleeping for {delay}")
|
log.debug(f"Sleeping for {delay}")
|
||||||
await trio.sleep(delay)
|
await trio.sleep(delay)
|
||||||
|
|
|
@ -17,7 +17,7 @@ from ..log import get_logger, colorize_json
|
||||||
import asks
|
import asks
|
||||||
asks.init('trio')
|
asks.init('trio')
|
||||||
|
|
||||||
log = get_logger('questrade')
|
log = get_logger(__name__)
|
||||||
|
|
||||||
_refresh_token_ep = 'https://login.questrade.com/oauth2/'
|
_refresh_token_ep = 'https://login.questrade.com/oauth2/'
|
||||||
_version = 'v1'
|
_version = 'v1'
|
||||||
|
@ -165,8 +165,8 @@ class Client:
|
||||||
|
|
||||||
return quotes
|
return quotes
|
||||||
|
|
||||||
async def symbols(self, tickers):
|
async def symbol_data(self, tickers: [str]):
|
||||||
"""Return quotes for each ticker in ``tickers``.
|
"""Return symbol data for ``tickers``.
|
||||||
"""
|
"""
|
||||||
t2ids = await self.tickers2ids(tickers)
|
t2ids = await self.tickers2ids(tickers)
|
||||||
ids = ','.join(map(str, t2ids.values()))
|
ids = ','.join(map(str, t2ids.values()))
|
||||||
|
|
|
@ -4,14 +4,15 @@ Robinhood API backend.
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from async_generator import asynccontextmanager
|
from async_generator import asynccontextmanager
|
||||||
|
# TODO: move to urllib3/requests once supported
|
||||||
import asks
|
import asks
|
||||||
|
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
from ._util import resproc
|
from ._util import resproc
|
||||||
from ..calc import percent_change
|
from ..calc import percent_change
|
||||||
|
|
||||||
log = get_logger('robinhood')
|
asks.init('trio')
|
||||||
|
log = get_logger(__name__)
|
||||||
_service_ep = 'https://api.robinhood.com'
|
_service_ep = 'https://api.robinhood.com'
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,15 +44,25 @@ class Client:
|
||||||
self._sess.base_location = _service_ep
|
self._sess.base_location = _service_ep
|
||||||
self.api = _API(self._sess)
|
self.api = _API(self._sess)
|
||||||
|
|
||||||
async def quote(self, symbols: [str]):
|
def _zip_in_order(self, symbols: [str], results_dict: dict):
|
||||||
results = (await self.api.quotes(','.join(symbols)))['results']
|
return {quote.get('symbol', sym) if quote else sym: quote
|
||||||
return {quote['symbol'] if quote else sym: quote
|
for sym, quote in zip(symbols, results_dict)}
|
||||||
for sym, quote in zip(symbols, results)}
|
|
||||||
|
|
||||||
async def symbols(self, tickers: [str]):
|
async def quote(self, symbols: [str]):
|
||||||
"""Placeholder for the watchlist calling code...
|
"""Retrieve quotes for a list of ``symbols``.
|
||||||
"""
|
"""
|
||||||
return {}
|
return self._zip_in_order(
|
||||||
|
symbols,
|
||||||
|
(await self.api.quotes(','.join(symbols)))['results']
|
||||||
|
)
|
||||||
|
|
||||||
|
async def symbol_data(self, symbols: [str]):
|
||||||
|
"""Retrieve symbol data via the ``fundmentals`` endpoint.
|
||||||
|
"""
|
||||||
|
return self._zip_in_order(
|
||||||
|
symbols,
|
||||||
|
(await self.api.fundamentals(','.join(symbols)))['results']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
|
|
13
piker/cli.py
13
piker/cli.py
|
@ -2,14 +2,13 @@
|
||||||
Console interface to broker client/daemons.
|
Console interface to broker client/daemons.
|
||||||
"""
|
"""
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from importlib import import_module
|
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import trio
|
import trio
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from .log import get_console_log, colorize_json, get_logger
|
from .log import get_console_log, colorize_json, get_logger
|
||||||
from .brokers import core
|
from .brokers import core, get_brokermod
|
||||||
|
|
||||||
log = get_logger('cli')
|
log = get_logger('cli')
|
||||||
DEFAULT_BROKER = 'robinhood'
|
DEFAULT_BROKER = 'robinhood'
|
||||||
|
@ -44,7 +43,7 @@ def api(meth, kwargs, loglevel, broker, keys):
|
||||||
"""client for testing broker API methods with pretty printing of output.
|
"""client for testing broker API methods with pretty printing of output.
|
||||||
"""
|
"""
|
||||||
log = get_console_log(loglevel)
|
log = get_console_log(loglevel)
|
||||||
brokermod = import_module('.' + broker, 'piker.brokers')
|
brokermod = get_brokermod(broker)
|
||||||
|
|
||||||
_kwargs = {}
|
_kwargs = {}
|
||||||
for kwarg in kwargs:
|
for kwarg in kwargs:
|
||||||
|
@ -77,11 +76,11 @@ def api(meth, kwargs, loglevel, broker, keys):
|
||||||
@click.option('--loglevel', '-l', default='warning', help='Logging level')
|
@click.option('--loglevel', '-l', default='warning', help='Logging level')
|
||||||
@click.option('--df-output', '-df', flag_value=True,
|
@click.option('--df-output', '-df', flag_value=True,
|
||||||
help='Ouput in `pandas.DataFrame` format')
|
help='Ouput in `pandas.DataFrame` format')
|
||||||
@click.argument('tickers', nargs=-1)
|
@click.argument('tickers', nargs=-1, required=True)
|
||||||
def quote(loglevel, broker, tickers, df_output):
|
def quote(loglevel, broker, tickers, df_output):
|
||||||
"""client for testing broker API methods with pretty printing of output.
|
"""client for testing broker API methods with pretty printing of output.
|
||||||
"""
|
"""
|
||||||
brokermod = import_module('.' + broker, 'piker.brokers')
|
brokermod = get_brokermod(broker)
|
||||||
quotes = run(partial(core.quote, brokermod, tickers), loglevel=loglevel)
|
quotes = run(partial(core.quote, brokermod, tickers), loglevel=loglevel)
|
||||||
if not quotes:
|
if not quotes:
|
||||||
log.error(f"No quotes could be found for {tickers}?")
|
log.error(f"No quotes could be found for {tickers}?")
|
||||||
|
@ -111,7 +110,7 @@ def watch(loglevel, broker, rate, name):
|
||||||
"""
|
"""
|
||||||
from .ui.watchlist import _async_main
|
from .ui.watchlist import _async_main
|
||||||
log = get_console_log(loglevel) # activate console logging
|
log = get_console_log(loglevel) # activate console logging
|
||||||
brokermod = import_module('.' + broker, 'piker.brokers')
|
brokermod = get_brokermod(broker)
|
||||||
|
|
||||||
watchlists = {
|
watchlists = {
|
||||||
'cannabis': [
|
'cannabis': [
|
||||||
|
@ -119,7 +118,7 @@ def watch(loglevel, broker, rate, name):
|
||||||
'CBW.VN', 'TRST.CN', 'VFF.TO', 'ACB.TO', 'ABCN.VN',
|
'CBW.VN', 'TRST.CN', 'VFF.TO', 'ACB.TO', 'ABCN.VN',
|
||||||
'APH.TO', 'MARI.CN', 'WMD.VN', 'LEAF.TO', 'THCX.VN',
|
'APH.TO', 'MARI.CN', 'WMD.VN', 'LEAF.TO', 'THCX.VN',
|
||||||
'WEED.TO', 'NINE.VN', 'RTI.VN', 'SNN.CN', 'ACB.TO',
|
'WEED.TO', 'NINE.VN', 'RTI.VN', 'SNN.CN', 'ACB.TO',
|
||||||
'OGI.VN', 'IMH.VN', 'FIRE.VN', 'EAT.CN', 'NUU.VN',
|
'OGI.VN', 'IMH.VN', 'FIRE.VN', 'EAT.CN',
|
||||||
'WMD.VN', 'HEMP.VN', 'CALI.CN', 'RQB.CN', 'MPX.CN',
|
'WMD.VN', 'HEMP.VN', 'CALI.CN', 'RQB.CN', 'MPX.CN',
|
||||||
'SEED.TO', 'HMJR.TO', 'CMED.TO', 'PAS.VN',
|
'SEED.TO', 'HMJR.TO', 'CMED.TO', 'PAS.VN',
|
||||||
'CRON',
|
'CRON',
|
||||||
|
|
|
@ -393,7 +393,7 @@ async def _async_main(name, tickers, brokermod, rate):
|
||||||
async with brokermod.get_client() as client:
|
async with brokermod.get_client() as client:
|
||||||
async with trio.open_nursery() as nursery:
|
async with trio.open_nursery() as nursery:
|
||||||
# get long term data including last days close price
|
# get long term data including last days close price
|
||||||
sd = await client.symbols(tickers)
|
sd = await client.symbol_data(tickers)
|
||||||
|
|
||||||
nursery.start_soon(
|
nursery.start_soon(
|
||||||
partial(poll_tickers, client, brokermod.quoter, tickers, queue,
|
partial(poll_tickers, client, brokermod.quoter, tickers, queue,
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
# matham's next-gen async port of kivy
|
# matham's next-gen async port of kivy
|
||||||
git+git://github.com/matham/kivy.git@async-loop
|
git+git://github.com/matham/kivy.git@async-loop#egg=kivy
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
"""
|
||||||
|
CLI testing, dawg.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def run(cmd):
|
||||||
|
"""Run cmd and check for zero return code.
|
||||||
|
"""
|
||||||
|
cp = subprocess.run(cmd.split())
|
||||||
|
cp.check_returncode()
|
||||||
|
return cp
|
||||||
|
|
||||||
|
|
||||||
|
def verify_keys(tickers, quotes_dict):
|
||||||
|
"""Verify all ticker names are keys in ``quotes_dict``.
|
||||||
|
"""
|
||||||
|
for key, quote in quotes_dict.items():
|
||||||
|
assert key in tickers
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def nyse_tickers():
|
||||||
|
"""List of well known NYSE ticker symbols.
|
||||||
|
"""
|
||||||
|
return ('TD', 'CRON', 'TSLA', 'AAPL')
|
||||||
|
|
||||||
|
|
||||||
|
def test_known_quotes(capfd, nyse_tickers):
|
||||||
|
"""Verify quotes are dumped to the console as json.
|
||||||
|
"""
|
||||||
|
run(f"piker quote {' '.join(nyse_tickers)}")
|
||||||
|
|
||||||
|
# verify output can be parsed as json
|
||||||
|
out, err = capfd.readouterr()
|
||||||
|
quotes_dict = json.loads(out)
|
||||||
|
verify_keys(nyse_tickers, quotes_dict)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'multiple_tickers',
|
||||||
|
[True, False]
|
||||||
|
)
|
||||||
|
def test_quotes_ticker_not_found(
|
||||||
|
capfd, caplog, nyse_tickers, multiple_tickers
|
||||||
|
):
|
||||||
|
"""Verify that if a ticker can't be found it's quote value is
|
||||||
|
``None`` and a warning log message is emitted to the console.
|
||||||
|
"""
|
||||||
|
bad_ticker = ('doggy',)
|
||||||
|
tickers = bad_ticker + nyse_tickers if multiple_tickers else bad_ticker
|
||||||
|
|
||||||
|
run(f"piker quote {' '.join(tickers)}")
|
||||||
|
|
||||||
|
out, err = capfd.readouterr()
|
||||||
|
if out:
|
||||||
|
# verify output can be parsed as json
|
||||||
|
quotes_dict = json.loads(out)
|
||||||
|
verify_keys(tickers, quotes_dict)
|
||||||
|
# check for warning log message when some quotes are found
|
||||||
|
warnmsg = f'Could not find symbol {bad_ticker[0]}'
|
||||||
|
assert warnmsg in err
|
||||||
|
else:
|
||||||
|
# when no quotes are found we should get an error message
|
||||||
|
errmsg = f'No quotes could be found for {bad_ticker}'
|
||||||
|
assert errmsg in err
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_method(nyse_tickers, capfd):
|
||||||
|
"""Ensure a low level api method can be called via CLI.
|
||||||
|
"""
|
||||||
|
run(f"piker api quotes symbols={','.join(nyse_tickers)}")
|
||||||
|
out, err = capfd.readouterr()
|
||||||
|
quotes_dict = json.loads(out)
|
||||||
|
assert isinstance(quotes_dict, dict)
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_method_not_found(nyse_tickers, capfd):
|
||||||
|
"""Ensure an error messages is printed when an API method isn't found.
|
||||||
|
"""
|
||||||
|
bad_meth = 'doggy'
|
||||||
|
run(f"piker api {bad_meth} names={' '.join(nyse_tickers)}")
|
||||||
|
out, err = capfd.readouterr()
|
||||||
|
assert 'null' in out
|
||||||
|
assert f'No api method `{bad_meth}` could be found?' in err
|
Loading…
Reference in New Issue