piker/piker/cli.py

209 lines
6.6 KiB
Python
Raw Normal View History

2018-01-26 01:54:13 +00:00
"""
Console interface to broker client/daemons.
"""
from functools import partial
2018-03-21 04:52:06 +00:00
from importlib import import_module
import os
2018-03-21 04:52:06 +00:00
from collections import defaultdict
import json
2018-01-26 01:54:13 +00:00
import click
import trio
import pandas as pd
2018-03-20 17:28:24 +00:00
from .log import get_console_log, colorize_json, get_logger
from . import watchlists as wl
2018-03-29 00:43:33 +00:00
from .brokers import core, get_brokermod
2018-03-20 17:28:24 +00:00
log = get_logger('cli')
DEFAULT_BROKER = 'robinhood'
2018-01-26 01:54:13 +00:00
2018-03-21 04:52:06 +00:00
_config_dir = click.get_app_dir('piker')
_watchlists_data_path = os.path.join(_config_dir, 'watchlists.json')
2018-01-26 01:54:13 +00:00
2018-04-03 03:55:02 +00:00
def run(main, loglevel='info'):
2018-01-26 01:54:13 +00:00
log = get_console_log(loglevel)
# main sandwich
2018-01-26 01:54:13 +00:00
try:
return trio.run(main)
2018-01-26 01:54:13 +00:00
except Exception as err:
log.exception(err)
finally:
log.debug("Exiting piker")
2018-01-26 01:54:13 +00:00
@click.group()
def cli():
pass
@cli.command()
@click.option('--broker', '-b', default=DEFAULT_BROKER,
help='Broker backend to use')
@click.option('--loglevel', '-l', default='warning', help='Logging level')
@click.option('--keys', '-k', multiple=True,
help='Return results only for these keys')
@click.argument('meth', nargs=1)
@click.argument('kwargs', nargs=-1)
def api(meth, kwargs, loglevel, broker, keys):
"""client for testing broker API methods with pretty printing of output.
"""
log = get_console_log(loglevel)
2018-03-27 20:27:30 +00:00
brokermod = get_brokermod(broker)
_kwargs = {}
for kwarg in kwargs:
if '=' not in kwarg:
log.error(f"kwarg `{kwarg}` must be of form <key>=<value>")
else:
key, _, value = kwarg.partition('=')
_kwargs[key] = value
data = run(
partial(core.api, brokermod, meth, **_kwargs), loglevel=loglevel)
if keys:
# filter to requested keys
filtered = []
if meth in data: # often a list of dicts
for item in data[meth]:
filtered.append({key: item[key] for key in keys})
else: # likely just a dict
filtered.append({key: data[key] for key in keys})
data = filtered
click.echo(colorize_json(data))
@cli.command()
@click.option('--broker', '-b', default=DEFAULT_BROKER,
help='Broker backend to use')
@click.option('--loglevel', '-l', default='warning', help='Logging level')
@click.option('--df-output', '-df', flag_value=True,
help='Ouput in `pandas.DataFrame` format')
2018-03-27 20:27:30 +00:00
@click.argument('tickers', nargs=-1, required=True)
def quote(loglevel, broker, tickers, df_output):
"""client for testing broker API methods with pretty printing of output.
"""
2018-03-27 20:27:30 +00:00
brokermod = get_brokermod(broker)
2018-03-20 17:28:24 +00:00
quotes = run(partial(core.quote, brokermod, tickers), loglevel=loglevel)
if not quotes:
log.error(f"No quotes could be found for {tickers}?")
return
cols = next(filter(bool, quotes.values())).copy()
cols.pop('symbol')
if df_output:
df = pd.DataFrame(
(quote or {} for quote in quotes.values()),
index=quotes.keys(),
columns=cols,
)
click.echo(df)
else:
2018-02-09 00:15:21 +00:00
click.echo(colorize_json(quotes))
@cli.command()
@click.option('--broker', '-b', default=DEFAULT_BROKER,
help='Broker backend to use')
@click.option('--loglevel', '-l', default='warning', help='Logging level')
@click.option('--rate', '-r', default=5, help='Logging level')
2018-03-14 18:00:24 +00:00
@click.argument('name', nargs=1, required=True)
def watch(loglevel, broker, rate, name):
"""Spawn a watchlist.
"""
from .ui.watchlist import _async_main
log = get_console_log(loglevel) # activate console logging
2018-03-27 20:27:30 +00:00
brokermod = get_brokermod(broker)
watchlist_from_file = wl.ensure_watchlists(_watchlists_data_path)
watchlists = wl.merge_watchlist(watchlist_from_file, wl._builtins)
broker_limit = getattr(brokermod, '_rate_limit', float('inf'))
if broker_limit < rate:
rate = broker_limit
log.warn(f"Limiting {brokermod.__name__} query rate to {rate}/sec")
trio.run(_async_main, name, watchlists[name], brokermod, rate)
2018-03-21 04:52:06 +00:00
@cli.group()
@click.option('--loglevel', '-l', default='warning', help='Logging level')
2018-04-03 03:55:02 +00:00
@click.option('--config_dir', '-d', default=_watchlists_data_path,
help='Path to piker configuration directory')
2018-03-21 04:52:06 +00:00
@click.pass_context
2018-04-03 03:55:02 +00:00
def watchlists(ctx, loglevel, config_dir):
"""Watchlists commands and operations
2018-03-21 04:52:06 +00:00
"""
get_console_log(loglevel) # activate console logging
wl.make_config_dir(_config_dir)
2018-04-03 03:55:02 +00:00
ctx.obj = {'path': config_dir,
'watchlist': wl.ensure_watchlists(config_dir)}
2018-03-21 04:52:06 +00:00
2018-03-21 04:52:06 +00:00
@watchlists.command(help='show watchlist')
@click.argument('name', nargs=1, required=False)
@click.pass_context
def show(ctx, name):
2018-04-03 03:55:02 +00:00
watchlist = ctx.obj['watchlist']
2018-03-21 04:52:06 +00:00
click.echo(colorize_json(
watchlist if name is None else watchlist[name]))
2018-03-21 04:52:06 +00:00
@watchlists.command(help='load passed in watchlist')
@click.argument('data', nargs=1, required=True)
@click.pass_context
def load(ctx, data):
try:
2018-04-06 19:07:47 +00:00
wl.write_to_file(json.loads(data), ctx.obj['path'])
except (json.JSONDecodeError, IndexError):
click.echo('You have passed an invalid text respresentation of a '
'JSON object. Try again.')
2018-03-21 04:52:06 +00:00
@watchlists.command(help='add ticker to watchlist')
@click.argument('name', nargs=1, required=True)
@click.argument('ticker_name', nargs=1, required=True)
@click.pass_context
def add(ctx, name, ticker_name):
watchlist = wl.add_ticker(name, ticker_name,
ctx.obj['watchlist'])
2018-04-06 19:07:47 +00:00
wl.write_to_file(watchlist, ctx.obj['path'])
2018-03-21 04:52:06 +00:00
@watchlists.command(help='remove ticker from watchlist')
@click.argument('name', nargs=1, required=True)
@click.argument('ticker_name', nargs=1, required=True)
@click.pass_context
def remove(ctx, name, ticker_name):
watchlist = wl.remove_ticker(name, ticker_name, ctx.obj['watchlist'])
2018-04-06 19:07:47 +00:00
wl.write_to_file(watchlist, ctx.obj['path'])
2018-03-21 04:52:06 +00:00
@watchlists.command(help='delete watchlist group')
2018-03-21 04:52:06 +00:00
@click.argument('name', nargs=1, required=True)
@click.pass_context
def delete(ctx, name):
watchlist = wl.delete_group(name, ctx.obj['watchlist'])
2018-04-06 19:07:47 +00:00
wl.write_to_file(watchlist, ctx.obj['path'])
2018-03-21 04:52:06 +00:00
@watchlists.command(help='merge a watchlist from another user')
@click.argument('watchlist_to_merge', nargs=1, required=True)
@click.pass_context
def merge(ctx, watchlist_to_merge):
merged_watchlist = wl.merge_watchlist(json.loads(watchlist_to_merge),
ctx.obj['watchlist'])
2018-04-06 19:07:47 +00:00
wl.write_to_file(merged_watchlist, ctx.obj['path'])
2018-03-21 04:52:06 +00:00
@watchlists.command(help='dump text respresentation of a watchlist to console')
2018-03-21 04:52:06 +00:00
@click.argument('name', nargs=1, required=False)
@click.pass_context
def dump(ctx, name):
click.echo(json.dumps(ctx.obj['watchlist']))