diff --git a/docs/source/remote_data.rst b/docs/source/remote_data.rst index c582fd99..0e5b6c82 100644 --- a/docs/source/remote_data.rst +++ b/docs/source/remote_data.rst @@ -29,6 +29,7 @@ extract data from various Internet sources into a pandas DataFrame. Currently the following sources are supported: - :ref:`Google Finance` + - :ref:`IEX` - :ref:`Enigma` - :ref:`Quandl` - :ref:`St.Louis FED (FRED)` @@ -64,6 +65,25 @@ Google Finance f = web.DataReader('F', 'google', start, end) f.ix['2010-01-04'] +.. _remote_data.iex: + +IEX +=== + +Historical stock prices from `IEX `__, + +.. ipython:: python + + import pandas_datareader.data as web + from datetime import datetime + start = datetime(2015, 2, 9) + end = datetime(2017, 5, 24) + f = web.DataReader('F', 'iex', start, end) + f.loc['2015-02-09'] + + +Prices are available up for the past 5 years. + .. _remote_data.enigma: Enigma diff --git a/pandas_datareader/data.py b/pandas_datareader/data.py index 76b79499..d9fd8db5 100644 --- a/pandas_datareader/data.py +++ b/pandas_datareader/data.py @@ -15,6 +15,7 @@ from pandas_datareader.google.daily import GoogleDailyReader from pandas_datareader.google.options import Options as GoogleOptions from pandas_datareader.google.quotes import GoogleQuotesReader +from pandas_datareader.iex.daily import IEXDailyReader from pandas_datareader.iex.deep import Deep as IEXDeep from pandas_datareader.iex.tops import LastReader as IEXLasts from pandas_datareader.iex.tops import TopsReader as IEXTops @@ -286,6 +287,13 @@ def DataReader(name, data_source=None, start=None, end=None, chunksize=25, retry_count=retry_count, pause=pause, session=session).read() + + elif data_source == "iex": + return IEXDailyReader(symbols=name, start=start, end=end, + chunksize=25, + retry_count=retry_count, pause=pause, + session=session).read() + elif data_source == "iex-tops": return IEXTops(symbols=name, start=start, end=end, retry_count=retry_count, pause=pause, diff --git a/pandas_datareader/iex/daily.py b/pandas_datareader/iex/daily.py new file mode 100644 index 00000000..46fda161 --- /dev/null +++ b/pandas_datareader/iex/daily.py @@ -0,0 +1,113 @@ +import datetime +import json + +import pandas as pd + +from dateutil.relativedelta import relativedelta +from pandas_datareader.base import _DailyBaseReader + +# Data provided for free by IEX +# Data is furnished in compliance with the guidelines promulgated in the IEX +# API terms of service and manual +# See https://iextrading.com/api-exhibit-a/ for additional information +# and conditions of use + + +class IEXDailyReader(_DailyBaseReader): + + """ + Returns DataFrame/Panel of historical stock prices from symbols, over date + range, start to end. To avoid being penalized by Google Finance servers, + pauses between downloading 'chunks' of symbols can be specified. + + Parameters + ---------- + symbols : string, array-like object (list, tuple, Series), or DataFrame + Single stock symbol (ticker), array-like object of symbols or + DataFrame with index containing stock symbols. + start : string, (defaults to '1/1/2010') + Starting date, timestamp. Parses many different kind of date + representations (e.g., 'JAN-01-2010', '1/1/10', 'Jan, 1, 1980') + end : string, (defaults to today) + Ending date, timestamp. Same format as starting date. + retry_count : int, default 3 + Number of times to retry query request. + pause : int, default 0 + Time, in seconds, to pause between consecutive queries of chunks. If + single value given for symbol, represents the pause between retries. + chunksize : int, default 25 + Number of symbols to download consecutively before intiating pause. + session : Session, default None + requests.sessions.Session instance to be used + """ + + def __init__(self, symbols=None, start=None, end=None, retry_count=3, + pause=0.35, session=None, chunksize=25): + super(IEXDailyReader, self).__init__(symbols=symbols, start=start, + end=end, retry_count=retry_count, + pause=pause, session=session, + chunksize=chunksize) + + @property + def url(self): + return 'https://api.iextrading.com/1.0/stock/market/batch' + + @property + def endpoint(self): + return "chart" + + def _get_params(self, symbol): + chart_range = self._range_string_from_date() + print(chart_range) + if isinstance(symbol, list): + symbolList = ','.join(symbol) + else: + symbolList = symbol + params = { + "symbols": symbolList, + "types": self.endpoint, + "range": chart_range, + } + return params + + def _range_string_from_date(self): + delta = relativedelta(self.start, datetime.datetime.now()) + if 2 <= (delta.years * -1) <= 5: + return "5y" + elif 1 <= (delta.years * -1) <= 2: + return "2y" + elif 0 <= (delta.years * -1) < 1: + return "1y" + else: + raise ValueError( + "Invalid date specified. Must be within past 5 years.") + + def read(self): + """read data""" + try: + return self._read_one_data(self.url, + self._get_params(self.symbols)) + finally: + self.close() + + def _read_lines(self, out): + data = out.read() + json_data = json.loads(data) + result = {} + if type(self.symbols) is str: + syms = [self.symbols] + else: + syms = self.symbols + for symbol in syms: + d = json_data.pop(symbol)["chart"] + df = pd.DataFrame(d) + df.set_index("date", inplace=True) + values = ["open", "high", "low", "close", "volume"] + df = df[values] + sstart = self.start.strftime('%Y-%m-%d') + send = self.end.strftime('%Y-%m-%d') + df = df.loc[sstart:send] + result.update({symbol: df}) + if len(result) > 1: + return result + return result[self.symbols] diff --git a/pandas_datareader/tests/test_iex_daily.py b/pandas_datareader/tests/test_iex_daily.py new file mode 100644 index 00000000..f6220783 --- /dev/null +++ b/pandas_datareader/tests/test_iex_daily.py @@ -0,0 +1,78 @@ +from datetime import datetime + +import pytest + +import pandas_datareader.data as web + + +class TestIEXDaily(object): + + @classmethod + def setup_class(cls): + pytest.importorskip("lxml") + + @property + def start(self): + return datetime(2015, 2, 9) + + @property + def end(self): + return datetime(2017, 5, 24) + + def test_iex_bad_symbol(self): + with pytest.raises(Exception): + web.DataReader("BADTICKER", "iex,", self.start, self.end) + + def test_iex_bad_symbol_list(self): + with pytest.raises(Exception): + web.DataReader(["AAPL", "BADTICKER"], "iex", self.start, self.end) + + def test_daily_invalid_date(self): + start = datetime(2010, 1, 5) + end = datetime(2017, 5, 24) + with pytest.raises(Exception): + web.DataReader(["AAPL", "TSLA"], "iex", start, end) + + def test_single_symbol(self): + df = web.DataReader("AAPL", "iex", self.start, self.end) + assert list(df) == ["open", "high", "low", "close", "volume"] + assert len(df) == 578 + assert df["volume"][-1] == 19219154 + + def test_multiple_symbols(self): + syms = ["AAPL", "MSFT", "TSLA"] + df = web.DataReader(syms, "iex", self.start, self.end) + assert sorted(list(df)) == syms + for sym in syms: + assert len(df[sym] == 578) + + def test_multiple_symbols_2(self): + syms = ["AAPL", "MSFT", "TSLA"] + good_start = datetime(2017, 2, 9) + good_end = datetime(2017, 5, 24) + df = web.DataReader(syms, "iex", good_start, good_end) + assert isinstance(df, dict) + assert len(df) == 3 + assert sorted(list(df)) == syms + + a = df["AAPL"] + t = df["TSLA"] + + assert len(a) == 73 + assert len(t) == 73 + + expected1 = a.loc["2017-02-09"] + assert expected1["close"] == 132.42 + assert expected1["high"] == 132.445 + + expected2 = a.loc["2017-05-24"] + assert expected2["close"] == 153.34 + assert expected2["high"] == 154.17 + + expected3 = t.loc["2017-02-09"] + assert expected3["close"] == 269.20 + assert expected3["high"] == 271.18 + + expected4 = t.loc["2017-05-24"] + assert expected4["close"] == 310.22 + assert expected4["high"] == 311.0