diff --git a/piker/brokers/core.py b/piker/brokers/core.py index e87b0ded..e660a9b5 100644 --- a/piker/brokers/core.py +++ b/piker/brokers/core.py @@ -65,9 +65,12 @@ async def poll_tickers( quotes = await get_quotes(tickers) postquote_start = time.time() payload = [] - for quote in quotes: + for symbol, quote in quotes.items(): + # TODO: uhh wtf? + if not quote: + continue - if quote['delay'] > 0: + if quote.get('delay', 0) > 0: log.warning(f"Delayed quote:\n{quote}") if diff_cached: diff --git a/piker/brokers/questrade.py b/piker/brokers/questrade.py index 021895f1..034ecd24 100644 --- a/piker/brokers/questrade.py +++ b/piker/brokers/questrade.py @@ -3,10 +3,12 @@ Questrade API backend. """ import time import datetime +from functools import partial import trio from async_generator import asynccontextmanager +from ..calc import humanize, percent_change from . import config from ._util import resproc, BrokerError from ..log import get_logger, colorize_json @@ -301,6 +303,88 @@ async def quoter(client: Client, tickers: [str]): else: raise - return quotes_resp['quotes'] + return {quote['symbol']: quote for quote in quotes_resp['quotes']} yield get_quote + + +# Questrade key conversion / column order +_qt_keys = { + 'symbol': 'symbol', # done manually in qtconvert + '%': '%', + 'lastTradePrice': 'last', + 'askPrice': 'ask', + 'bidPrice': 'bid', + 'lastTradeSize': 'size', + 'bidSize': 'bsize', + 'askSize': 'asize', + 'VWAP': ('VWAP', partial(round, ndigits=3)), + 'mktcap': ('mktcap', humanize), + '$ vol': ('$ vol', humanize), + 'volume': ('vol', humanize), + 'close': 'close', + 'openPrice': 'open', + 'lowPrice': 'low', + 'highPrice': 'high', + # 'low52w': 'low52w', # put in info widget + # 'high52w': 'high52w', + # "lastTradePriceTrHrs": 7.99, + # "lastTradeTick": "Equal", + # "lastTradeTime": "2018-01-30T18:28:23.434000-05:00", + # "symbolId": 3575753, + # "tier": "", + # 'isHalted': 'halted', # as subscript 'h' + # 'delay': 'delay', # as subscript 'p' +} + +_bidasks = { + 'last': ['bid', 'ask'], + 'size': ['bsize', 'asize'], + 'VWAP': ['low', 'high'], +} + + +def format_quote( + quote: dict, + symbol_data: dict, + keymap: dict = _qt_keys, +) -> (dict, dict): + """Remap a list of quote dicts ``quotes`` using the mapping of old keys + -> new keys ``keymap`` returning 2 dicts: one with raw data and the other + for display. + + Returns 2 dicts: first is the original values mapped by new keys, + and the second is the same but with all values converted to a + "display-friendly" string format. + """ + last = quote['lastTradePrice'] + symbol = quote['symbol'] + previous = symbol_data[symbol]['prevDayClosePrice'] + change = percent_change(previous, last) + share_count = symbol_data[symbol].get('outstandingShares', None) + mktcap = share_count * last if share_count else 'NA' + computed = { + 'symbol': quote['symbol'], + '%': round(change, 3), + 'mktcap': mktcap, + '$ vol': round(quote['VWAP'] * quote['volume'], 3), + 'close': previous, + } + new = {} + displayable = {} + + for key, new_key in keymap.items(): + display_value = value = computed.get(key) or quote.get(key) + + # API servers can return `None` vals when markets are closed (weekend) + value = 0 if value is None else value + + # convert values to a displayble format using available formatting func + if isinstance(new_key, tuple): + new_key, func = new_key + display_value = func(value) + + new[new_key] = value + displayable[new_key] = display_value + + return new, displayable diff --git a/piker/brokers/robinhood.py b/piker/brokers/robinhood.py index 5a6b7de8..405f1fa0 100644 --- a/piker/brokers/robinhood.py +++ b/piker/brokers/robinhood.py @@ -1,11 +1,14 @@ """ Robinhood API backend. """ +from functools import partial + from async_generator import asynccontextmanager import asks from ..log import get_logger from ._util import resproc +from ..calc import percent_change log = get_logger('robinhood') @@ -45,9 +48,113 @@ class Client: return {quote['symbol'] if quote else sym: quote for sym, quote in zip(symbols, results)} + async def symbols(self, tickers: [str]): + """Placeholder for the watchlist calling code... + """ + return {} + @asynccontextmanager async def get_client() -> Client: """Spawn a RH broker client. """ yield Client() + + +@asynccontextmanager +async def quoter(client: Client, tickers: [str]): + """Quoter context. + """ + yield client.quote + + +# Robinhood key conversion / column order +_rh_keys = { + 'symbol': 'symbol', # done manually in qtconvert + '%': '%', + 'last_trade_price': ('last', partial(round, ndigits=3)), + 'last_extended_hours_trade_price': 'last pre-mkt', + 'ask_price': ('ask', partial(round, ndigits=3)), + 'bid_price': ('bid', partial(round, ndigits=3)), + # 'lastTradeSize': 'size', # not available? + 'bid_size': 'bsize', + 'ask_size': 'asize', + # 'VWAP': ('VWAP', partial(round, ndigits=3)), + # 'mktcap': ('mktcap', humanize), + # '$ vol': ('$ vol', humanize), + # 'volume': ('vol', humanize), + 'previous_close': 'close', + 'adjusted_previous_close': 'adj close', + # 'trading_halted': 'halted', + + # example fields + # "adjusted_previous_close": "8.1900", + # "ask_price": "8.2800", + # "ask_size": 1200, + # "bid_price": "8.2500", + # "bid_size": 1800, + # "has_traded": true, + # "last_extended_hours_trade_price": null, + # "last_trade_price": "8.2350", + # "last_trade_price_source": "nls", + # "previous_close": "8.1900", + # "previous_close_date": "2018-03-20", + # "symbol": "CRON", + # "trading_halted": false, + # "updated_at": "2018-03-21T13:46:05Z" +} + +_bidasks = { + 'last': ['bid', 'ask'], + # 'size': ['bsize', 'asize'], + # 'VWAP': ['low', 'high'], + # 'last pre-mkt': ['close', 'adj close'], +} + + +def format_quote( + quote: dict, symbol_data: dict, + keymap: dict = _rh_keys, +) -> (dict, dict): + """remap a list of quote dicts ``quotes`` using the mapping of old keys + -> new keys ``keymap`` returning 2 dicts: one with raw data and the other + for display. + + returns 2 dicts: first is the original values mapped by new keys, + and the second is the same but with all values converted to a + "display-friendly" string format. + """ + last = quote['last_trade_price'] + # symbol = quote['symbol'] + previous = quote['previous_close'] + change = percent_change(float(previous), float(last)) + # share_count = symbol_data[symbol].get('outstandingshares', none) + # mktcap = share_count * last if share_count else 'na' + computed = { + 'symbol': quote['symbol'], + '%': round(change, 3), + 'ask_price': float(quote['ask_price']), + 'bid_price': float(quote['bid_price']), + 'last_trade_price': float(quote['last_trade_price']), + # 'mktcap': mktcap, + # '$ vol': round(quote['vwap'] * quote['volume'], 3), + 'close': previous, + } + new = {} + displayable = {} + + for key, new_key in keymap.items(): + display_value = value = computed.get(key) or quote.get(key) + + # api servers can return `None` vals when markets are closed (weekend) + value = 0 if value is None else value + + # convert values to a displayble format using available formatting func + if isinstance(new_key, tuple): + new_key, func = new_key + display_value = func(value) + + new[new_key] = value + displayable[new_key] = display_value + + return new, displayable