diff --git a/docs/source/readers/index.rst b/docs/source/readers/index.rst index 26eca39d..bcedd791 100644 --- a/docs/source/readers/index.rst +++ b/docs/source/readers/index.rst @@ -15,6 +15,7 @@ Data Readers nasdaq-trader oecd quandl + robinhood stooq tsp world-bank diff --git a/docs/source/readers/robinhood.rst b/docs/source/readers/robinhood.rst new file mode 100644 index 00000000..ca109015 --- /dev/null +++ b/docs/source/readers/robinhood.rst @@ -0,0 +1,12 @@ +Robinhood +--------- + +.. py:module:: pandas_datareader.robinhood + +.. autoclass:: RobinhoodHistoricalReader + :members: + :inherited-members: + +.. autoclass:: RobinhoodQuoteReader + :members: + :inherited-members: diff --git a/docs/source/remote_data.rst b/docs/source/remote_data.rst index 2c120388..481e2141 100644 --- a/docs/source/remote_data.rst +++ b/docs/source/remote_data.rst @@ -31,6 +31,7 @@ Currently the following sources are supported: - :ref:`Google Finance` - :ref:`Morningstar` - :ref:`IEX` + - :ref:`Robinhood` - :ref:`Enigma` - :ref:`Quandl` - :ref:`St.Louis FED (FRED)` @@ -110,6 +111,22 @@ A third interface to the deep API is exposed through f = web.DataReader('gs', 'iex-tops') f[:10] + +.. _remote_data.robinhood: + +Robinhood +========= +`Robinhood `__ is a stock trading platform with an +API that provides a limited set of data. Historical daily data is limited to 1 +year relative to today. + +.. ipython:: python + + import pandas_datareader.data as web + from datetime import datetime + f = web.DataReader('F', 'robinhood') + f.head() + .. _remote_data.enigma: Enigma diff --git a/docs/source/whatsnew/v0.6.0.txt b/docs/source/whatsnew/v0.6.0.txt index 4e88e9f4..88065746 100644 --- a/docs/source/whatsnew/v0.6.0.txt +++ b/docs/source/whatsnew/v0.6.0.txt @@ -1,6 +1,6 @@ .. _whatsnew_060: -v0.6.0 (January 23, 2018) +v0.6.0 (January 24, 2018) --------------------------- This is a major release from 0.5.0. We recommend that all users upgrade. @@ -24,7 +24,11 @@ Highlights include: have been removed. PDR would like to restore these features, and pull requests are welcome. -- A new connector for Morningstart Open, High, Low, Close and Volume was +- A new connector for Robinhood was introduced. This provides + up to 1 year of historical end-of-day data. It also provides near + real-time quotes. (:issue:`477`). + +- A new connector for Morningstar Open, High, Low, Close and Volume was introduced (:issue:`467`) - A new connector for IEX daily price data was introduced (:issue:`465`). @@ -47,7 +51,6 @@ Highlights include: Enhancements ~~~~~~~~~~~~ - - A new data connector for data provided by the `Bank of Canada `__ was introduced. (:issue:`440`) @@ -66,6 +69,9 @@ Enhancements - A new data connector for stock pricing data provided by `Morningstar `__ was introduced. (:issue:`467`) +- A new data connector for stock pricing data provided by + `Robinhood `__ was introduced. (:issue:`477`) + .. _whatsnew_060.api_breaking: Backwards incompatible API changes diff --git a/pandas_datareader/data.py b/pandas_datareader/data.py index 7cbab349..d7926c96 100644 --- a/pandas_datareader/data.py +++ b/pandas_datareader/data.py @@ -24,6 +24,8 @@ from pandas_datareader.nasdaq_trader import get_nasdaq_symbols from pandas_datareader.oecd import OECDReader from pandas_datareader.quandl import QuandlReader +from pandas_datareader.robinhood import RobinhoodHistoricalReader, \ + RobinhoodQuoteReader from pandas_datareader.stooq import StooqDailyReader from pandas_datareader.yahoo.actions import (YahooActionReader, YahooDivReader) from pandas_datareader.yahoo.components import _get_data as \ @@ -40,7 +42,8 @@ 'get_recent_iex', 'get_markets_iex', 'get_last_iex', 'get_iex_symbols', 'get_iex_book', 'get_dailysummary_iex', 'get_data_morningstar', 'get_data_stooq', - 'get_data_stooq', 'DataReader'] + 'get_data_stooq', 'get_data_robinhood', 'get_quotes_robinhood', + 'DataReader'] def get_data_fred(*args, **kwargs): @@ -103,6 +106,14 @@ def get_data_morningstar(*args, **kwargs): return MorningstarDailyReader(*args, **kwargs).read() +def get_data_robinhood(*args, **kwargs): + return RobinhoodHistoricalReader(*args, **kwargs).read() + + +def get_quotes_robinhood(*args, **kwargs): + return RobinhoodQuoteReader(*args, **kwargs).read() + + def get_markets_iex(*args, **kwargs): """ Returns near-real time volume data across markets segregated by tape @@ -369,7 +380,10 @@ def DataReader(name, data_source=None, start=None, end=None, return MorningstarDailyReader(symbols=name, start=start, end=end, retry_count=retry_count, pause=pause, session=session, interval="d").read() - + elif data_source == 'robinhood': + return RobinhoodHistoricalReader(symbols=name, start=start, end=end, + retry_count=retry_count, pause=pause, + session=session).read() else: msg = "data_source=%r is not implemented" % data_source raise NotImplementedError(msg) diff --git a/pandas_datareader/robinhood.py b/pandas_datareader/robinhood.py new file mode 100644 index 00000000..6e965c8f --- /dev/null +++ b/pandas_datareader/robinhood.py @@ -0,0 +1,155 @@ +import pandas as pd + +from pandas_datareader.base import _BaseReader + + +class RobinhoodQuoteReader(_BaseReader): + """ + Read quotes from Robinhood + + Parameters + ---------- + symbols : {str, List[str]} + String symbol of like of symbols + start : None + Quotes are near real-time and so this value is ignored + end : None + Quotes are near real-time and so this value is ignored + retry_count : int, default 3 + Number of times to retry query request. + pause : float, default 0.1 + Time, in seconds, of the pause between retries. + session : Session, default None + requests.sessions.Session instance to be used + freq : None + Quotes are near real-time and so this value is ignored + """ + _format = 'json' + + def __init__(self, symbols, start=None, end=None, retry_count=3, pause=.1, + timeout=30, session=None, freq=None): + super(RobinhoodQuoteReader, self).__init__(symbols, start, end, + retry_count, pause, + timeout, session, freq) + if isinstance(self.symbols, str): + self.symbols = [self.symbols] + self._max_symbols = 1630 + self._validate_symbols() + self._json_results = [] + + def _validate_symbols(self): + if len(self.symbols) > self._max_symbols: + raise ValueError('A maximum of {0} symbols are supported ' + 'in a single call.'.format(self._max_symbols)) + + def _get_crumb(self, *args): + pass + + @property + def url(self): + """API URL""" + return 'https://api.robinhood.com/quotes/' + + @property + def params(self): + """Parameters to use in API calls""" + symbols = ','.join(self.symbols) + return {'symbols': symbols} + + def _process_json(self): + res = pd.DataFrame(self._json_results) + return res.set_index('symbol').T + + def _read_lines(self, out): + if 'next' in out: + self._json_results.extend(out['results']) + return self._read_one_data(out['next']) + self._json_results.extend(out['results']) + return self._process_json() + + +class RobinhoodHistoricalReader(RobinhoodQuoteReader): + """ + Read historical values from Robinhood + + Parameters + ---------- + symbols : {str, List[str]} + String symbol of like of symbols + start : None + Ignored. See span and interval. + end : None + Ignored. See span and interval. + retry_count : int, default 3 + Number of times to retry query request. + pause : float, default 0.1 + Time, in seconds, of the pause between retries. + session : Session, default None + requests.sessions.Session instance to be used + freq : None + Quotes are near real-time and so this value is ignored + interval : {'day' ,'week', '5minute', '10minute'} + Interval between historical prices + span : {'day', 'week', 'year', '5year'} + Time span relative to now to retrieve. The available spans are a + function of interval. See notes + + Notes + ----- + Only provides up to 1 year of daily data. + + The available spans are a function of interval. + + * day: year + * week: 5year + * 5minute: day, week + * 10minute: day, week + """ + _format = 'json' + + def __init__(self, symbols, start=None, end=None, retry_count=3, pause=.1, + timeout=30, session=None, freq=None, interval='day', + span='year'): + super(RobinhoodHistoricalReader, self).__init__(symbols, start, end, + retry_count, pause, + timeout, session, freq) + interval_span = {'day': ['year'], + 'week': ['5year'], + '10minute': ['day', 'week'], + '5minute': ['day', 'week']} + if interval not in interval_span: + raise ValueError('Interval must be one of ' + '{0}'.format(', '.join(interval_span.keys()))) + valid_spans = interval_span[interval] + if span not in valid_spans: + raise ValueError('For interval {0}, span must ' + 'be in: {1}'.format(interval, valid_spans)) + self.interval = interval + self.span = span + self._max_symbols = 75 + self._validate_symbols() + self._json_results = [] + + @property + def url(self): + """API URL""" + return 'https://api.robinhood.com/quotes/historicals/' + + @property + def params(self): + """Parameters to use in API calls""" + symbols = ','.join(self.symbols) + pars = {'symbols': symbols, + 'interval': self.interval, + 'span': self.span} + + return pars + + def _process_json(self): + df = [] + for sym in self._json_results: + vals = pd.DataFrame(sym['historicals']) + vals['begins_at'] = pd.to_datetime(vals['begins_at']) + vals['symbol'] = sym['symbol'] + df.append(vals.set_index(['symbol', 'begins_at'])) + return pd.concat(df, 0) diff --git a/pandas_datareader/tests/test_robinhood.py b/pandas_datareader/tests/test_robinhood.py new file mode 100644 index 00000000..5d9ff19b --- /dev/null +++ b/pandas_datareader/tests/test_robinhood.py @@ -0,0 +1,48 @@ +import numpy as np +import pandas as pd +import pytest + +from pandas_datareader.robinhood import RobinhoodQuoteReader, \ + RobinhoodHistoricalReader + +syms = ['GOOG', ['GOOG', 'AAPL']] +ids = list(map(str, syms)) + + +@pytest.fixture(params=['GOOG', ['GOOG', 'AAPL']], ids=ids) +def symbols(request): + return request.param + + +def test_robinhood_quote(symbols): + df = RobinhoodQuoteReader(symbols=symbols).read() + assert isinstance(df, pd.DataFrame) + if isinstance(symbols, str): + symbols = [symbols] + assert df.shape[1] == len(symbols) + + +def test_robinhood_quote_too_many(): + syms = np.random.randint(65, 90, size=(10000, 4)).tolist() + syms = list(map(lambda r: ''.join(map(chr, r)), syms)) + syms = list(set(syms)) + with pytest.raises(ValueError): + RobinhoodQuoteReader(symbols=syms) + + +def test_robinhood_historical_too_many(): + syms = np.random.randint(65, 90, size=(10000, 4)).tolist() + syms = list(map(lambda r: ''.join(map(chr, r)), syms)) + syms = list(set(syms)) + with pytest.raises(ValueError): + RobinhoodHistoricalReader(symbols=syms) + with pytest.raises(ValueError): + RobinhoodHistoricalReader(symbols=syms[:76]) + + +def test_robinhood_historical(symbols): + df = RobinhoodHistoricalReader(symbols=symbols).read() + assert isinstance(df, pd.DataFrame) + if isinstance(symbols, str): + symbols = [symbols] + assert df.index.levels[0].shape[0] == len(symbols)