Skip to content

Commit

Permalink
ENH: Add support for Robinhood API
Browse files Browse the repository at this point in the history
Add support for reading quotes and historical from Robinhood
  • Loading branch information
bashtage committed Jan 24, 2018
1 parent 39b5b16 commit d6b2531
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 5 deletions.
1 change: 1 addition & 0 deletions docs/source/readers/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Data Readers
nasdaq-trader
oecd
quandl
robinhood
stooq
tsp
world-bank
12 changes: 12 additions & 0 deletions docs/source/readers/robinhood.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Robinhood
---------

.. py:module:: pandas_datareader.robinhood
.. autoclass:: RobinhoodHistoricalReader
:members:
:inherited-members:

.. autoclass:: RobinhoodQuoteReader
:members:
:inherited-members:
17 changes: 17 additions & 0 deletions docs/source/remote_data.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Currently the following sources are supported:
- :ref:`Google Finance<remote_data.google>`
- :ref:`Morningstar<remote_data.morningstar>`
- :ref:`IEX<remote_data.iex>`
- :ref:`Robinhood<remote_data.robinhood>`
- :ref:`Enigma<remote_data.enigma>`
- :ref:`Quandl<remote_data.quandl>`
- :ref:`St.Louis FED (FRED)<remote_data.fred>`
Expand Down Expand Up @@ -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 <https://www.robinhood.com>`__ 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
Expand Down
12 changes: 9 additions & 3 deletions docs/source/whatsnew/v0.6.0.txt
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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`).
Expand All @@ -47,7 +51,6 @@ Highlights include:
Enhancements
~~~~~~~~~~~~


- A new data connector for data provided by the
`Bank of Canada <https://www.bankofcanada.ca/rates/>`__ was introduced.
(:issue:`440`)
Expand All @@ -66,6 +69,9 @@ Enhancements
- A new data connector for stock pricing data provided by
`Morningstar <http://www.morningstar.com>`__ was introduced. (:issue:`467`)

- A new data connector for stock pricing data provided by
`Robinhood <https://www.robinhood.com>`__ was introduced. (:issue:`477`)

.. _whatsnew_060.api_breaking:

Backwards incompatible API changes
Expand Down
18 changes: 16 additions & 2 deletions pandas_datareader/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
155 changes: 155 additions & 0 deletions pandas_datareader/robinhood.py
Original file line number Diff line number Diff line change
@@ -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)
48 changes: 48 additions & 0 deletions pandas_datareader/tests/test_robinhood.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit d6b2531

Please sign in to comment.