198 lines
6.2 KiB
Python
198 lines
6.2 KiB
Python
# piker: trading gear for hackers
|
|
# Copyright (C) 2018-present Tyler Goodlet (in stewardship of piker0)
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
"""
|
|
Robinhood API backend.
|
|
|
|
WARNING: robinhood now requires authenticated access to use the quote
|
|
endpoints (it didn't originally). We need someone with a valid US
|
|
account to test this code.
|
|
"""
|
|
from functools import partial
|
|
from typing import List
|
|
|
|
from async_generator import asynccontextmanager
|
|
import asks
|
|
|
|
from ..log import get_logger
|
|
from ._util import resproc, BrokerError
|
|
from ..calc import percent_change
|
|
|
|
log = get_logger(__name__)
|
|
|
|
_service_ep = 'https://api.robinhood.com'
|
|
|
|
|
|
class _API:
|
|
"""Robinhood API endpoints exposed as methods and wrapped with an
|
|
http session.
|
|
"""
|
|
def __init__(self, session: asks.Session):
|
|
self._sess = session
|
|
|
|
async def _request(self, path: str, params=None) -> dict:
|
|
resp = await self._sess.get(path=f'/{path}', params=params)
|
|
return resproc(resp, log)
|
|
|
|
async def quotes(self, symbols: str) -> dict:
|
|
return await self._request('quotes/', params={'symbols': symbols})
|
|
|
|
async def fundamentals(self, symbols: str) -> dict:
|
|
return await self._request(
|
|
'fundamentals/', params={'symbols': symbols})
|
|
|
|
|
|
class Client:
|
|
"""API client suitable for use as a long running broker daemon or
|
|
single api requests.
|
|
"""
|
|
def __init__(self):
|
|
self._sess = asks.Session()
|
|
self._sess.base_location = _service_ep
|
|
self.api = _API(self._sess)
|
|
|
|
def _zip_in_order(self, symbols: [str], quotes: List[dict]):
|
|
return {quote.get('symbol', sym) if quote else sym: quote
|
|
for sym, quote in zip(symbols, results_dict)}
|
|
|
|
async def quote(self, symbols: [str]):
|
|
"""Retrieve quotes for a list of ``symbols``.
|
|
"""
|
|
try:
|
|
quotes = (await self.api.quotes(','.join(symbols)))['results']
|
|
except BrokerError:
|
|
quotes = [None] * len(symbols)
|
|
|
|
for quote in quotes:
|
|
# insert our subscription key field
|
|
if quote is not None:
|
|
quote['key'] = quote['symbol']
|
|
|
|
return list(filter(bool, quotes))
|
|
|
|
async def symbol_data(self, symbols: [str]):
|
|
"""Retrieve symbol data via the ``fundamentals`` endpoint.
|
|
"""
|
|
return self._zip_in_order(
|
|
symbols,
|
|
(await self.api.fundamentals(','.join(symbols)))['results']
|
|
)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def get_client() -> Client:
|
|
"""Spawn a RH broker client.
|
|
"""
|
|
yield Client()
|
|
|
|
|
|
async def quoter(client: Client, tickers: [str]):
|
|
"""Quoter context.
|
|
"""
|
|
return 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
|