diff --git a/pandas_datareader/__init__.py b/pandas_datareader/__init__.py index e51b863c..1f9d59af 100644 --- a/pandas_datareader/__init__.py +++ b/pandas_datareader/__init__.py @@ -1,7 +1,11 @@ from ._version import get_versions + from .data import (get_components_yahoo, get_data_famafrench, get_data_google, get_data_yahoo, get_data_enigma, get_data_yahoo_actions, - get_quote_google, get_quote_yahoo, DataReader, Options) + get_quote_google, get_quote_yahoo, get_tops_iex, + get_last_iex, get_markets_iex, get_summary_iex, + get_records_iex, get_recent_iex, get_iex_symbols, + get_iex_book, DataReader, Options) __version__ = get_versions()['version'] del get_versions @@ -9,4 +13,7 @@ __all__ = ['__version__', 'get_components_yahoo', 'get_data_enigma', 'get_data_famafrench', 'get_data_google', 'get_data_yahoo', 'get_data_yahoo_actions', 'get_quote_google', 'get_quote_yahoo', + 'get_iex_book', 'get_iex_symbols', 'get_last_iex', + 'get_markets_iex', 'get_recent_iex', 'get_records_iex', + 'get_summary_iex', 'get_tops_iex', 'DataReader', 'Options'] diff --git a/pandas_datareader/base.py b/pandas_datareader/base.py index e3efbe24..a76e33eb 100644 --- a/pandas_datareader/base.py +++ b/pandas_datareader/base.py @@ -133,14 +133,29 @@ def _get_response(self, url, params=None, headers=None): # Get a new breadcrumb if necessary, in case ours is invalidated if isinstance(params, list) and 'crumb' in params: params['crumb'] = self._get_crumb(self.retry_count) + + # If our output error function returns True, exit the loop. + if self._output_error(response): + break + if params is not None and len(params) > 0: url = url + "?" + urlencode(params) + raise RemoteDataError('Unable to read URL: {0}'.format(url)) def _get_crumb(self, *args): """ To be implemented by subclass """ raise NotImplementedError("Subclass has not implemented method.") + def _output_error(self, out): + """If necessary, a service can implement an interpreter for any non-200 + HTTP responses. + + :param out: raw output from an HTTP request + :return: boolean + """ + return False + def _read_lines(self, out): rs = read_csv(out, index_col=0, parse_dates=True, na_values=('-', 'null'))[::-1] diff --git a/pandas_datareader/data.py b/pandas_datareader/data.py index 60205490..b40bf6de 100644 --- a/pandas_datareader/data.py +++ b/pandas_datareader/data.py @@ -13,6 +13,9 @@ 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.deep import Deep as IEXDeep +from pandas_datareader.iex.tops import LastReader as IEXLasts +from pandas_datareader.iex.tops import TopsReader as IEXTops from pandas_datareader.moex import MoexReader from pandas_datareader.nasdaq_trader import get_nasdaq_symbols from pandas_datareader.oecd import OECDReader @@ -29,6 +32,9 @@ 'get_data_fred', 'get_data_google', 'get_data_moex', 'get_data_quandl', 'get_data_yahoo', 'get_data_yahoo_actions', 'get_nasdaq_symbols', 'get_quote_google', 'get_quote_yahoo', + 'get_tops_iex', 'get_summary_iex', 'get_records_iex', + 'get_recent_iex', 'get_markets_iex', 'get_last_iex', + 'get_iex_symbols', 'get_iex_book', 'get_dailysummary_iex', 'get_data_stooq', 'DataReader'] @@ -76,6 +82,129 @@ def get_data_stooq(*args, **kwargs): return StooqDailyReader(*args, **kwargs).read() +def get_tops_iex(*args, **kwargs): + return IEXTops(*args, **kwargs).read() + + +def get_last_iex(*args, **kwargs): + return IEXLasts(*args, **kwargs).read() + + +def get_markets_iex(*args, **kwargs): + """ + Returns near-real time volume data across markets segregated by tape + and including a percentage of overall volume during the session + + This endpoint does not accept any parameters. + + Reference: https://www.iextrading.com/developer/docs/#markets + + :return: DataFrame + """ + from pandas_datareader.iex.market import MarketReader + return MarketReader(*args, **kwargs).read() + + +def get_dailysummary_iex(*args, **kwargs): + """ + Returns a summary of daily market volume statistics. Without parameters, + this will return the most recent trading session by default. + + :param start: + A datetime object - the beginning of the date range. + :param end: + A datetime object - the end of the date range. + + Reference: https://www.iextrading.com/developer/docs/#historical-daily + + :return: DataFrame + """ + from pandas_datareader.iex.stats import DailySummaryReader + return DailySummaryReader(*args, **kwargs).read() + + +def get_summary_iex(*args, **kwargs): + """ + Returns an aggregated monthly summary of market volume and a variety of + related metrics for trades by lot size, security market cap, and venue. + In the absence of parameters, this will return month-to-date statistics. + For ranges spanning multiple months, this will return one row per month. + + :param start: + A datetime object - the beginning of the date range. + :param end: + A datetime object - the end of the date range. + + :return: DataFrame + """ + from pandas_datareader.iex.stats import MonthlySummaryReader + return MonthlySummaryReader(*args, **kwargs).read() + + +def get_records_iex(*args, **kwargs): + """ + Returns the record value, record date, recent value, and 30-day average for + market volume, # of symbols traded, # of routed trades and notional value. + This function accepts no additional parameters. + + Reference: https://www.iextrading.com/developer/docs/#records + + :return: DataFrame + """ + from pandas_datareader.iex.stats import RecordsReader + return RecordsReader(*args, **kwargs).read() + + +def get_recent_iex(*args, **kwargs): + """ + Returns market volume and trade routing statistics for recent sessions. + Also reports IEX's relative market share, lit share volume and a boolean + halfday indicator. + + Reference: https://www.iextrading.com/developer/docs/#recent + + :return: DataFrame + """ + from pandas_datareader.iex.stats import RecentReader + return RecentReader(*args, **kwargs).read() + + +def get_iex_symbols(*args, **kwargs): + """ + Returns a list of all equity symbols available for trading on IEX. Accepts + no additional parameters. + + Reference: https://www.iextrading.com/developer/docs/#symbols + + :return: DataFrame + """ + from pandas_datareader.iex.ref import SymbolsReader + return SymbolsReader(*args, **kwargs).read() + + +def get_iex_book(*args, **kwargs): + """ + Returns an array of dictionaries with depth of book data from IEX for up to + 10 securities at a time. Returns a dictionary of the bid and ask books. + + :param symbols: + A string or list of strings of valid tickers + :param service: + 'book': Live depth of book data + 'op-halt-status': Checks to see if the exchange has instituted a halt + 'security-event': Denotes individual security related event + 'ssr-status': Short Sale Price Test restrictions, per reg 201 of SHO + 'system-event': Relays current feed status (i.e. market open) + 'trades': Retrieves recent executions, trade size/price and flags + 'trade-breaks': Lists execution breaks for the current trading session + 'trading-status': Returns status and cause codes for securities + + :return: Object + """ + from pandas_datareader.iex.deep import Deep + return Deep(*args, **kwargs).read() + + def DataReader(name, data_source=None, start=None, end=None, retry_count=3, pause=0.001, session=None, access_key=None): """ @@ -103,6 +232,8 @@ def DataReader(name, data_source=None, start=None, end=None, single value given for symbol, represents the pause between retries. session : Session, default None requests.sessions.Session instance to be used + access_key : (str, None) + Optional parameter to specify an API key for certain data sources. Examples ---------- @@ -117,6 +248,13 @@ def DataReader(name, data_source=None, start=None, end=None, # Data from Google Finance aapl = DataReader("AAPL", "google") + # Price and volume data from IEX + tops = DataReader(["GS", "AAPL"], "iex-tops") + # Top of book executions from IEX + gs = DataReader("GS", "iex-last") + # Real-time depth of book data from IEX + gs = DataReader("GS", "iex-book") + # Data from FRED vix = DataReader("VIXCLS", "fred") @@ -140,6 +278,7 @@ def DataReader(name, data_source=None, start=None, end=None, return YahooActionReader(symbols=name, start=start, end=end, retry_count=retry_count, pause=pause, session=session).read() + elif data_source == "yahoo-dividends": return YahooDivReader(symbols=name, start=start, end=end, adjust_price=False, chunksize=25, @@ -151,6 +290,15 @@ 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-tops": + return IEXTops(symbols=name, start=start, end=end, + retry_count=retry_count, pause=pause, + session=session).read() + + elif data_source == "iex-last": + return IEXLasts(symbols=name, start=start, end=end, + retry_count=retry_count, pause=pause, + session=session).read() elif data_source == "bankofcanada": return BankOfCanadaReader(symbols=name, start=start, end=end, @@ -162,6 +310,11 @@ def DataReader(name, data_source=None, start=None, end=None, retry_count=retry_count, pause=pause, session=session).read() + elif data_source == "iex-book": + return IEXDeep(symbols=name, service="book", start=start, end=end, + retry_count=retry_count, pause=pause, + session=session).read() + elif data_source == "enigma": return EnigmaReader(dataset_id=name, api_key=access_key).read() diff --git a/pandas_datareader/exceptions.py b/pandas_datareader/exceptions.py new file mode 100644 index 00000000..41c347ce --- /dev/null +++ b/pandas_datareader/exceptions.py @@ -0,0 +1,5 @@ +"""Custom warnings and exceptions""" + + +class UnstableAPIWarning(Warning): + pass diff --git a/pandas_datareader/iex/__init__.py b/pandas_datareader/iex/__init__.py new file mode 100644 index 00000000..befc7284 --- /dev/null +++ b/pandas_datareader/iex/__init__.py @@ -0,0 +1,83 @@ +import json + +import pandas as pd +from pandas.io.common import urlencode +from pandas_datareader.base import _BaseReader + + +# 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 IEX(_BaseReader): + """ + Serves as the base class for all IEX API services. + """ + + _format = 'json' + + def __init__(self, symbols=None, start=None, end=None, retry_count=3, + pause=0.001, session=None): + super(IEX, self).__init__(symbols=symbols, + start=start, end=end, + retry_count=retry_count, + pause=pause, session=session) + + @property + def service(self): + # This property will be overridden by the subclass + raise NotImplementedError("IEX API service not specified.") + + @property + def url(self): + qstring = urlencode(self._get_params(self.symbols)) + return "https://api.iextrading.com/1.0/{}?{}".format(self.service, + qstring) + + def read(self): + df = super(IEX, self).read() + if isinstance(df, pd.DataFrame): + df = df.squeeze() + if not isinstance(df, pd.DataFrame): + df = pd.DataFrame(df) + return df + + def _get_params(self, symbols): + p = {} + if isinstance(symbols, list): + p['symbols'] = ','.join(symbols) + elif isinstance(symbols, str): + p['symbols'] = symbols + return p + + def _output_error(self, out): + """If IEX returns a non-200 status code, we need to notify the user of + the error returned. + + :param out: Raw HTTP Output + """ + try: + content = json.loads(out.text) + except Exception: + raise TypeError("Failed to interpret response as JSON.") + + for key, string in content.items(): + e = "IEX Output error encountered: {}".format(string) + if key == 'error': + raise Exception(e) + + def _read_lines(self, out): + """IEX's output does not need anything complex, so we're overriding to + use Pandas' default interpreter + + :param out: Raw HTTP Output + :return: DataFrame + """ + + # IEX will return a blank line for invalid tickers: + if isinstance(out, list): + out = [x for x in out if x is not None] + return pd.DataFrame(out) if len(out) > 0 else pd.DataFrame() diff --git a/pandas_datareader/iex/deep.py b/pandas_datareader/iex/deep.py new file mode 100644 index 00000000..dd5adf9d --- /dev/null +++ b/pandas_datareader/iex/deep.py @@ -0,0 +1,126 @@ +from pandas_datareader.iex import IEX +from datetime import datetime + +# 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 Deep(IEX): + def __init__(self, symbols=None, service=None, start=None, end=None, + retry_count=3, pause=0.001, session=None): + if isinstance(symbols, str): + symbols = symbols.lower() + else: + symbols = [s.lower() for s in symbols] + + super(Deep, self).__init__(symbols=symbols, + start=start, end=end, + retry_count=retry_count, + pause=pause, session=session) + self.sub = service + + @property + def service(self): + ss = "/" + self.sub if self.sub is not None else "" + return "deep{}".format(ss) + + def _read_lines(self, out): + """ + IEX depth of book data varies and shouldn't always be returned in a DF + + :param out: Raw HTTP Output + :return: DataFrame + """ + + # Runs appropriate output functions per the service being accessed. + fmap = { + 'book': '_pass', + 'op-halt-status': '_convert_tstamp', + 'security-event': '_convert_tstamp', + 'ssr-status': '_convert_tstamp', + 'system-event': '_read_system_event', + 'trades': '_pass', + 'trade-breaks': '_convert_tstamp', + 'trading-status': '_read_trading_status', + None: '_pass', + } + + if self.sub in fmap: + return getattr(self, fmap[self.sub])(out) + else: + raise "Invalid service specified: {}.".format(self.sub) + + def _read_system_event(self, out): + # Map the response code to a string output per the API docs. + # Per: https://www.iextrading.com/developer/docs/#system-event-message + smap = { + 'O': 'Start of messages', + 'S': 'Start of system hours', + 'R': 'Start of regular market hours', + 'M': 'End of regular market hours', + 'E': 'End of system hours', + 'C': 'End of messages' + } + tid = out["systemEvent"] + out["eventResponse"] = smap[tid] + + return self._convert_tstamp(out) + + @staticmethod + def _pass(out): + return out + + def _read_trading_status(self, out): + # Reference: https://www.iextrading.com/developer/docs/#trading-status + smap = { + 'H': 'Trading halted across all US equity markets', + 'O': 'Trading halt released into an Order Acceptance Period ' + '(IEX-listed securities only)', + 'P': 'Trading paused and Order Acceptance Period on IEX ' + '(IEX-listed securities only)', + 'T': 'Trading on IEX' + } + rmap = { + # Trading Halt Reasons + 'T1': 'Halt News Pending', + 'IPO1': 'IPO/New Issue Not Yet Trading', + 'IPOD': 'IPO/New Issue Deferred', + 'MCB3': 'Market-Wide Circuit Breaker Level 3 - Breached', + 'NA': 'Reason Not Available', + + # Order Acceptance Period Reasons + 'T2': 'Halt News Dissemination', + 'IPO2': 'IPO/New Issue Order Acceptance Period', + 'IPO3': 'IPO Pre-Launch Period', + 'MCB1': 'Market-Wide Circuit Breaker Level 1 - Breached', + 'MCB2': 'Market-Wide Circuit Breaker Level 2 - Breached' + } + for ticker, data in out.items(): + if data['status'] in smap: + data['statusText'] = smap[data['status']] + + if data['reason'] in rmap: + data['reasonText'] = rmap[data['reason']] + + out[ticker] = data + + return self._convert_tstamp(out) + + @staticmethod + def _convert_tstamp(out): + # Searches for top-level timestamp attributes or within dictionaries + if 'timestamp' in out: + # Convert UNIX to datetime object + f = float(out["timestamp"]) + out["timestamp"] = datetime.fromtimestamp(f/1000) + else: + for ticker, data in out.items(): + if 'timestamp' in data: + f = float(data["timestamp"]) + data["timestamp"] = datetime.fromtimestamp(f/1000) + out[ticker] = data + + return out diff --git a/pandas_datareader/iex/market.py b/pandas_datareader/iex/market.py new file mode 100644 index 00000000..a16a3e0e --- /dev/null +++ b/pandas_datareader/iex/market.py @@ -0,0 +1,24 @@ +from pandas_datareader.iex import IEX + +# 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 MarketReader(IEX): + def __init__(self, symbols=None, start=None, end=None, retry_count=3, + pause=0.001, session=None): + super(MarketReader, self).__init__(symbols=symbols, + start=start, end=end, + retry_count=retry_count, + pause=pause, session=session) + + @property + def service(self): + return "market" + + def _get_params(self, symbols): + # Market API does not take any parameters, returning empty dict + return {} diff --git a/pandas_datareader/iex/ref.py b/pandas_datareader/iex/ref.py new file mode 100644 index 00000000..ab7d928a --- /dev/null +++ b/pandas_datareader/iex/ref.py @@ -0,0 +1,24 @@ +from pandas_datareader.iex import IEX + +# 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 SymbolsReader(IEX): + def __init__(self, symbols=None, start=None, end=None, retry_count=3, + pause=0.001, session=None): + super(SymbolsReader, self).__init__(symbols=symbols, + start=start, end=end, + retry_count=retry_count, + pause=pause, session=session) + + @property + def service(self): + return "ref-data/symbols" + + def _get_params(self, symbols): + # Ref Data API does not take any parameters, returning empty dict + return {} diff --git a/pandas_datareader/iex/stats.py b/pandas_datareader/iex/stats.py new file mode 100644 index 00000000..4fa5793f --- /dev/null +++ b/pandas_datareader/iex/stats.py @@ -0,0 +1,144 @@ +from datetime import datetime, timedelta + +import pandas as pd + +from pandas_datareader.exceptions import UnstableAPIWarning +from pandas_datareader.iex import IEX + + +# 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 DailySummaryReader(IEX): + def __init__(self, symbols=None, start=None, end=None, retry_count=3, + pause=0.001, session=None): + import warnings + warnings.warn('Daily statistics is not working due to issues with the ' + 'IEX API', UnstableAPIWarning) + self.curr_date = start + super(DailySummaryReader, self).__init__(symbols=symbols, + start=start, end=end, + retry_count=retry_count, + pause=pause, session=session) + + @property + def service(self): + return "stats/historical/daily" + + def _get_params(self, symbols): + p = {} + + if self.curr_date is not None: + p['date'] = self.curr_date.strftime('%Y%m%d') + + return p + + def read(self): + """Unfortunately, IEX's API can only retrieve data one day or one month + at a time. Rather than specifying a date range, we will have to run + the read function for each date provided. + + :return: DataFrame + """ + tlen = self.end - self.start + dfs = [] + for date in (self.start + timedelta(n) for n in range(tlen.days)): + self.curr_date = date + tdf = super(IEX, self).read() + dfs.append(tdf) + return pd.concat(dfs) + + +class MonthlySummaryReader(IEX): + def __init__(self, symbols=None, start=None, end=None, retry_count=3, + pause=0.001, session=None): + self.curr_date = start + self.date_format = '%Y%m' + + super(MonthlySummaryReader, self).__init__(symbols=symbols, + start=start, end=end, + retry_count=retry_count, + pause=pause, + session=session) + + @property + def service(self): + return "stats/historical" + + def _get_params(self, symbols): + p = {} + + if self.curr_date is not None: + p['date'] = self.curr_date.strftime(self.date_format) + + return p + + def read(self): + """Unfortunately, IEX's API can only retrieve data one day or one month + at a time. Rather than specifying a date range, we will have to run + the read function for each date provided. + + :return: DataFrame + """ + tlen = self.end - self.start + dfs = [] + + # Build list of all dates within the given range + lrange = [x for x in (self.start + timedelta(n) + for n in range(tlen.days))] + + mrange = [] + for dt in lrange: + if datetime(dt.year, dt.month, 1) not in mrange: + mrange.append(datetime(dt.year, dt.month, 1)) + lrange = mrange + + for date in lrange: + self.curr_date = date + tdf = super(IEX, self).read() + + # We may not return data if this was a weekend/holiday: + if not tdf.empty: + tdf['date'] = date.strftime(self.date_format) + dfs.append(tdf) + + # We may not return any data if we failed to specify useful parameters: + return pd.concat(dfs) if len(dfs) > 0 else pd.DataFrame() + + +class RecordsReader(IEX): + def __init__(self, symbols=None, start=None, end=None, retry_count=3, + pause=0.001, session=None): + super(RecordsReader, self).__init__(symbols=symbols, + start=start, end=end, + retry_count=retry_count, + pause=pause, session=session) + + @property + def service(self): + return "stats/records" + + def _get_params(self, symbols): + # Record Stats API does not take any parameters, returning empty dict + return {} + + +class RecentReader(IEX): + def __init__(self, symbols=None, start=None, end=None, retry_count=3, + pause=0.001, session=None): + super(RecentReader, self).__init__(symbols=symbols, + start=start, end=end, + retry_count=retry_count, + pause=pause, session=session) + + @property + def service(self): + return "stats/recent" + + def _get_params(self, symbols): + # Record Stats API does not take any parameters, returning empty dict + return {} diff --git a/pandas_datareader/iex/tops.py b/pandas_datareader/iex/tops.py new file mode 100644 index 00000000..02e49dfa --- /dev/null +++ b/pandas_datareader/iex/tops.py @@ -0,0 +1,35 @@ +from pandas_datareader.iex import IEX + +# 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 TopsReader(IEX): + + def __init__(self, symbols=None, start=None, end=None, retry_count=3, + pause=0.001, session=None): + super(TopsReader, self).__init__(symbols=symbols, + start=start, end=end, + retry_count=retry_count, + pause=pause, session=session) + + @property + def service(self): + return "tops" + + +class LastReader(IEX): + # todo: Eventually we'll want to implement WebSockets as an option. + def __init__(self, symbols=None, start=None, end=None, retry_count=3, + pause=0.001, session=None): + super(LastReader, self).__init__(symbols=symbols, + start=start, end=end, + retry_count=retry_count, + pause=pause, session=session) + + @property + def service(self): + return "tops/last" diff --git a/pandas_datareader/tests/test_data.py b/pandas_datareader/tests/test_data.py index 83e6393e..989e4a64 100644 --- a/pandas_datareader/tests/test_data.py +++ b/pandas_datareader/tests/test_data.py @@ -20,6 +20,10 @@ def test_read_google(self): gs = DataReader("GS", "google") assert isinstance(gs, DataFrame) + def test_read_iex(self): + gs = DataReader("GS", "iex-last") + assert isinstance(gs, DataFrame) + def test_read_fred(self): vix = DataReader("VIXCLS", "fred") assert isinstance(vix, DataFrame) diff --git a/pandas_datareader/tests/test_iex.py b/pandas_datareader/tests/test_iex.py new file mode 100644 index 00000000..ac38e03f --- /dev/null +++ b/pandas_datareader/tests/test_iex.py @@ -0,0 +1,48 @@ +from datetime import datetime + +import pytest +from pandas import DataFrame + +from pandas_datareader.data import (DataReader, get_summary_iex, get_last_iex, + get_dailysummary_iex, get_iex_symbols, + get_iex_book) + + +class TestIEX(object): + @classmethod + def setup_class(cls): + pytest.importorskip("lxml") + + def test_read_iex(self): + gs = DataReader("GS", "iex-last") + assert isinstance(gs, DataFrame) + + def test_historical(self): + df = get_summary_iex(start=datetime(2017, 4, 1), + end=datetime(2017, 4, 30)) + assert df["averageDailyVolume"].iloc[0] == 137650908.9 + + def test_false_ticker(self): + df = get_last_iex("INVALID TICKER") + assert df.shape[0] == 0 + + @pytest.mark.xfail(reason='IEX daily history API is returning 500 as of ' + 'Jan 2018') + def test_daily(self): + df = get_dailysummary_iex(start=datetime(2017, 5, 5), + end=datetime(2017, 5, 6)) + assert df['routedVolume'].iloc[0] == 39974788 + + def test_symbols(self): + df = get_iex_symbols() + assert 'GS' in df.symbol.values + + def test_live_prices(self): + dftickers = get_iex_symbols() + tickers = dftickers[:5].symbol.values + df = get_last_iex(tickers[:5]) + assert df["price"].mean() > 0 + + def test_deep(self): + dob = get_iex_book('GS', service='book') + assert 'GS' in dob