Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: Add support for Robinhood API #477

Merged
merged 1 commit into from
Jan 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)