Move quote formatting to broker backends
parent
f0149118e1
commit
6b47130c77
|
@ -65,9 +65,12 @@ async def poll_tickers(
|
||||||
quotes = await get_quotes(tickers)
|
quotes = await get_quotes(tickers)
|
||||||
postquote_start = time.time()
|
postquote_start = time.time()
|
||||||
payload = []
|
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}")
|
log.warning(f"Delayed quote:\n{quote}")
|
||||||
|
|
||||||
if diff_cached:
|
if diff_cached:
|
||||||
|
|
|
@ -3,10 +3,12 @@ Questrade API backend.
|
||||||
"""
|
"""
|
||||||
import time
|
import time
|
||||||
import datetime
|
import datetime
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
from async_generator import asynccontextmanager
|
from async_generator import asynccontextmanager
|
||||||
|
|
||||||
|
from ..calc import humanize, percent_change
|
||||||
from . import config
|
from . import config
|
||||||
from ._util import resproc, BrokerError
|
from ._util import resproc, BrokerError
|
||||||
from ..log import get_logger, colorize_json
|
from ..log import get_logger, colorize_json
|
||||||
|
@ -301,6 +303,88 @@ async def quoter(client: Client, tickers: [str]):
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
return quotes_resp['quotes']
|
return {quote['symbol']: quote for quote in quotes_resp['quotes']}
|
||||||
|
|
||||||
yield get_quote
|
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
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
"""
|
"""
|
||||||
Robinhood API backend.
|
Robinhood API backend.
|
||||||
"""
|
"""
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from async_generator import asynccontextmanager
|
from async_generator import asynccontextmanager
|
||||||
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
|
||||||
|
|
||||||
log = get_logger('robinhood')
|
log = get_logger('robinhood')
|
||||||
|
|
||||||
|
@ -45,9 +48,113 @@ class Client:
|
||||||
return {quote['symbol'] if quote else sym: quote
|
return {quote['symbol'] if quote else sym: quote
|
||||||
for sym, quote in zip(symbols, results)}
|
for sym, quote in zip(symbols, results)}
|
||||||
|
|
||||||
|
async def symbols(self, tickers: [str]):
|
||||||
|
"""Placeholder for the watchlist calling code...
|
||||||
|
"""
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def get_client() -> Client:
|
async def get_client() -> Client:
|
||||||
"""Spawn a RH broker client.
|
"""Spawn a RH broker client.
|
||||||
"""
|
"""
|
||||||
yield 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
|
||||||
|
|
Loading…
Reference in New Issue