diff --git a/tiingo/api.py b/tiingo/api.py index ce6bf51..49702a4 100644 --- a/tiingo/api.py +++ b/tiingo/api.py @@ -1,655 +1,512 @@ -# -*- coding: utf-8 -*- - -from collections import namedtuple -import csv -import json -import os -import re -import sys -import pkg_resources -from zipfile import ZipFile - -import requests - -from tiingo.restclient import RestClient -from tiingo.exceptions import ( - InstallPandasException, - APIColumnNameError, - InvalidFrequencyError, - MissingRequiredArgumentError) - -try: - import pandas as pd - pandas_is_installed = True -except ImportError: - pandas_is_installed = False - -VERSION = pkg_resources.get_distribution("tiingo").version - - -# These methods enable python 2 + 3 compatibility. -def get_zipfile_from_response(response): - if sys.version_info < (3, 0): # python 2 - from StringIO import StringIO as Buffer - else: # python 3 - from io import BytesIO as Buffer - buffered = Buffer(response.content) - return ZipFile(buffered) - - -def get_buffer_from_zipfile(zipfile, filename): - if sys.version_info < (3, 0): # python 2 - from StringIO import StringIO - return StringIO(zipfile.read(filename)) - else: # python 3 - # Source: - # https://stackoverflow.com/questions/5627954/py3k-how-do-you-read-a-file-inside-a-zip-file-as-text-not-bytes - from io import (TextIOWrapper, BytesIO) - return TextIOWrapper(BytesIO(zipfile.read(filename))) - - -def dict_to_object(item, object_name): - """Converts a python dict to a namedtuple, saving memory.""" - fields = item.keys() - values = item.values() - return json.loads(json.dumps(item), - object_hook=lambda d: - namedtuple(object_name, fields)(*values)) - - -class TiingoClient(RestClient): - """Class for managing interactions with the Tiingo REST API - - Supply API Key via Environment Variable TIINGO_API_KEY - or via the Config Object - """ - - def __init__(self, *args, **kwargs): - super(TiingoClient, self).__init__(*args, **kwargs) - self._base_url = "https://api.tiingo.com" - - try: - api_key = self._config['api_key'] - except KeyError: - api_key = os.environ.get('TIINGO_API_KEY') - self._api_key = api_key - - if not(api_key): - raise RuntimeError("Tiingo API Key not provided. Please provide" - " via environment variable or config argument.") - - self._headers = { - 'Authorization': "Token {}".format(api_key), - 'Content-Type': 'application/json', - 'User-Agent': 'tiingo-python-client {}'.format(VERSION) - } - - self._frequency_pattern = re.compile('^[0-9]+(min|hour)$', re.IGNORECASE) - - def __repr__(self): - return ''.format(self._base_url) - - def _is_eod_frequency(self,frequency): - return frequency.lower() in ['daily', 'weekly', 'monthly', 'annually'] - - # TICKER PRICE ENDPOINTS - # https://api.tiingo.com/docs/tiingo/daily - def list_tickers(self, assetTypes=[]): - """Return a list of dicts of metadata tickers for all supported tickers - of the specified asset type, as well as metadata about each ticker. - This includes supported date range, the exchange the ticker is traded - on, and the currency the stock is traded on. - Tickers for unrelated products are omitted. - https://apimedia.tiingo.com/docs/tiingo/daily/supported_tickers.zip - """ - listing_file_url = "https://apimedia.tiingo.com/docs/tiingo/daily/supported_tickers.zip" - response = requests.get(listing_file_url) - zipdata = get_zipfile_from_response(response) - raw_csv = get_buffer_from_zipfile(zipdata, 'supported_tickers.csv') - reader = csv.DictReader(raw_csv) - - if not len(assetTypes): - return [row for row in reader] - - assetTypesSet = set(assetTypes) - return [row for row in reader - if row.get('assetType') in assetTypesSet] - - def list_stock_tickers(self): - return self.list_tickers(['Stock']) - - def list_etf_tickers(self): - return self.list_tickers(['ETF']) - - def list_fund_tickers(self): - return self.list_tickers(['Mutual Fund']) - - def get_ticker_metadata(self, ticker, fmt='json'): - """Return metadata for 1 ticker - Use TiingoClient.list_tickers() to get available options - - Args: - ticker (str) : Unique identifier for stock - """ - url = "tiingo/daily/{}".format(ticker) - response = self._request('GET', url) - data = response.json() - if fmt == 'json': - return data - elif fmt == 'object': - return dict_to_object(data, "Ticker") - - def _invalid_frequency(self, frequency): - """ - Check to see that frequency was specified correctly - :param frequency (string): frequency string - :return (boolean): - """ - is_valid = self._is_eod_frequency(frequency) or re.match(self._frequency_pattern, frequency) - return not is_valid - - def _get_url(self, ticker, frequency): - """ - Return url based on frequency. Daily, weekly, or yearly use Tiingo - EOD api; anything less than daily uses the iex intraday api. - :param ticker (string): ticker to be embedded in the url - :param frequency (string): valid frequency per Tiingo api - :return (string): url - """ - if self._invalid_frequency(frequency): - etext = ("Error: {} is an invalid frequency. Check Tiingo API documentation " - "for valid EOD or intraday frequency format.") - raise InvalidFrequencyError(etext.format(frequency)) - else: - if self._is_eod_frequency(frequency): - return "tiingo/daily/{}/prices".format(ticker) - else: - return "iex/{}/prices".format(ticker) - - def _request_pandas(self, ticker, metric_name, params): - """ - Return data for ticker as a pandas.DataFrame if metric_name is not - specified or as a pandas.Series if metric_name is specified. - - :param ticker (string): ticker to be requested - :param params (dict): a dictionary containing valid resampleFreq - and format strings per the Tiingo api - :param metric_name (string): Optional parameter specifying metric to be returned for each - ticker. In the event of a single ticker, this is optional and if not specified - all of the available data will be returned. In the event of a list of tickers, - this parameter is required. - """ - url = self._get_url(ticker, params['resampleFreq']) - response = self._request('GET', url, params=params) - if params['format'] == 'csv': - if sys.version_info < (3, 0): # python 2 - from StringIO import StringIO - else: # python 3 - from io import StringIO - - df = pd.read_csv(StringIO(response.content.decode('utf-8'))) - else: - df = pd.DataFrame(response.json()) - - df.set_index('date', inplace=True) - - if metric_name is not None: - prices = df[metric_name] - else: - prices = df - - prices.index = pd.to_datetime(prices.index) - - # Localize to UTC to ensure equivalence between data returned in json format and - # csv format. Tiingo daily data requested in csv format does not include a timezone. - if prices.index.tz is None: - prices.index = prices.index.tz_localize('UTC') - - return prices - - def get_ticker_price(self, ticker, - startDate=None, endDate=None, - fmt='json', frequency='daily'): - """By default, return latest EOD Composite Price for a stock ticker. - On average, each feed contains 3 data sources. - - Supported tickers + Available Day Ranges are here: - https://apimedia.tiingo.com/docs/tiingo/daily/supported_tickers.zip - - Args: - ticker (string): Unique identifier for stock ticker - startDate (string): Start of ticker range in YYYY-MM-DD format - endDate (string): End of ticker range in YYYY-MM-DD format - fmt (string): 'csv' or 'json' - frequency (string): Resample frequency - """ - url = self._get_url(ticker, frequency) - params = { - 'format': fmt if fmt != "object" else 'json', # conversion local - 'resampleFreq': frequency - } - - if startDate: - params['startDate'] = startDate - if endDate: - params['endDate'] = endDate - - # TODO: evaluate whether to stream CSV to cache on disk, or - # load as array in memory, or just pass plain text - response = self._request('GET', url, params=params) - if fmt == "json": - return response.json() - elif fmt == "object": - data = response.json() - return [dict_to_object(item, "TickerPrice") for item in data] - else: - return response.content.decode("utf-8") - - def get_dataframe(self, tickers, - startDate=None, endDate=None, metric_name=None, - frequency='daily', fmt='json'): - - """ Return a pandas.DataFrame of historical prices for one or more ticker symbols. - - By default, return latest EOD Composite Price for a list of stock tickers. - On average, each feed contains 3 data sources. - - Supported tickers + Available Day Ranges are here: - https://apimedia.tiingo.com/docs/tiingo/daily/supported_tickers.zip - or from the TiingoClient.list_tickers() method. - - Args: - tickers (string/list): One or more unique identifiers for a stock ticker. - startDate (string): Start of ticker range in YYYY-MM-DD format. - endDate (string): End of ticker range in YYYY-MM-DD format. - metric_name (string): Optional parameter specifying metric to be returned for each - ticker. In the event of a single ticker, this is optional and if not specified - all of the available data will be returned. In the event of a list of tickers, - this parameter is required. - frequency (string): Resample frequency (defaults to daily). - fmt (string): 'csv' or 'json' - """ - - valid_columns = {'open', 'high', 'low', 'close', 'volume', 'adjOpen', 'adjHigh', 'adjLow', - 'adjClose', 'adjVolume', 'divCash', 'splitFactor'} - - if metric_name is not None and metric_name not in valid_columns: - raise APIColumnNameError('Valid data items are: ' + str(valid_columns)) - - if metric_name is None and isinstance(tickers, list): - raise MissingRequiredArgumentError("""When tickers is provided as a list, metric_name is a required argument. - Please provide a metric_name, or call this method with one ticker at a time.""") - - params = { - 'format': fmt, - 'resampleFreq': frequency - } - if startDate: - params['startDate'] = startDate - if endDate: - params['endDate'] = endDate - - if pandas_is_installed: - if type(tickers) is str: - prices = self._request_pandas( - ticker=tickers, params=params, metric_name=metric_name) - else: - prices = pd.DataFrame() - for stock in tickers: - ticker_series = self._request_pandas( - ticker=stock, params=params, metric_name=metric_name) - ticker_series = ticker_series.rename(stock) - prices = pd.concat([prices, ticker_series], axis=1, sort=True) - - return prices - - else: - error_message = ("Pandas is not installed, but .get_ticker_price() was " - "called with fmt=pandas. In order to install tiingo with " - "pandas, reinstall with pandas as an optional dependency. \n" - "Install tiingo with pandas dependency: \'pip install tiingo[pandas]\'\n" - "Alternatively, just install pandas: pip install pandas.") - raise InstallPandasException(error_message) - -####################### BEGIN Modifications (GenusGeoff) -### Add get_daily_data and get_iex_data -### -### The suffix _data is more appropriate in this case because if a metric_name is passed then the method returns a -### pandas.Series not a pandas.DataFrame. -### - - # Get Daily Data - def get_daily_data(self, tickers, - startDate=None, endDate=None, metric_name=None, - frequency='daily', fmt='csv'): - - """ Returns historical prices for one or more ticker symbols. - - By default, return latest EOD Composite Adjusted Closing Price for a list of stock tickers. - On average, each feed contains 3 data sources. - - Supported tickers + Available Day Ranges are here: - https://apimedia.tiingo.com/docs/tiingo/daily/supported_tickers.zip - or from the TiingoClient.list_tickers() method. - - Args: - tickers (string/list): One or more unique identifiers for a stock ticker. - startDate (string): Start of ticker range in YYYY-MM-DD format. - endDate (string): End of ticker range in YYYY-MM-DD format. - metric_name (string): Optional parameter specifying metric to be returned for each - ticker. In the event of a single ticker, this is optional and if not specified - all of the available data will be returned. In the event of a list of tickers, - this parameter is required. - frequency (string): Resample frequency (defaults to daily). - fmt (string): 'csv' or 'json' - """ - valid_columns = {'open', 'high', 'low', 'close', 'volume', 'adjOpen', 'adjHigh', 'adjLow', - 'adjClose', 'adjVolume', 'divCash', 'splitFactor'} - - if metric_name is not None and metric_name not in valid_columns: - raise APIColumnNameError('Valid data items are: ' + str(valid_columns)) - - if metric_name is None and isinstance(tickers, list): - raise MissingRequiredArgumentError("""When tickers is provided as a list, metric_name is a required argument. - Please provide a metric_name, or call this method with one ticker at a time.""") - - params = { - 'format': fmt, - 'resampleFreq': frequency - } - if startDate: - params['startDate'] = startDate - if endDate: - params['endDate'] = endDate - - if pandas_is_installed: - if type(tickers) is str: - prices = self._request_pandas( - ticker=tickers, params=params, metric_name=metric_name) - else: - prices = pd.DataFrame() - for stock in tickers: - ticker_series = self._request_pandas( - ticker=stock, params=params, metric_name=metric_name) - ticker_series = ticker_series.rename(stock) - prices = pd.concat([prices, ticker_series], axis=1, sort=True) - - return prices - - else: - error_message = ("Pandas is not installed, but .get_ticker_price() was " - "called with fmt=pandas. In order to install tiingo with " - "pandas, reinstall with pandas as an optional dependency. \n" - "Install tiingo with pandas dependency: \'pip install tiingo[pandas]\'\n" - "Alternatively, just install pandas: pip install pandas.") - raise InstallPandasException(error_message) - -## Get IEX Data - - def get_iex_data(self, tickers, - startDate=None, endDate=None, metric_name=None, - frequency='1hour', fmt='csv'): - - """ Return a pandas.DataFrame of historical prices for one or more ticker symbols. - - By default, return latest EOD Composite Price for a list of stock tickers. - On average, each feed contains 3 data sources. - - Supported tickers + Available Day Ranges are here: - https://apimedia.tiingo.com/docs/tiingo/daily/supported_tickers.zip - or from the TiingoClient.list_tickers() method. - - Args: - tickers (string/list): One or more unique identifiers for a stock ticker. - startDate (string): Start of ticker range in YYYY-MM-DD format. - endDate (string): End of ticker range in YYYY-MM-DD format. - metric_name (string): Optional parameter specifying metric to be returned for each - ticker. In the event of a single ticker, this is optional and if not specified - all of the available data will be returned. In the event of a list of tickers, - this parameter is required. - frequency (string): Resample frequency (defaults to daily). - fmt (string): 'csv' or 'json' - """ - - valid_columns = {'open', 'high', 'low', 'close', 'volume'} - - if metric_name is not None and metric_name not in valid_columns: - raise APIColumnNameError('Valid data items are: ' + str(valid_columns)) - - if metric_name is None and isinstance(tickers, list): - raise MissingRequiredArgumentError("""When tickers is provided as a list, metric_name is a required argument. - Please provide a metric_name, or call this method with one ticker at a time.""") - - params = { - 'format': fmt, - 'resampleFreq': frequency - } - if startDate: - params['startDate'] = startDate - if endDate: - params['endDate'] = endDate - - if pandas_is_installed: - if type(tickers) is str: - prices = self._request_pandas( - ticker=tickers, params=params, metric_name=metric_name) - else: - prices = pd.DataFrame() - for stock in tickers: - ticker_series = self._request_pandas( - ticker=stock, params=params, metric_name=metric_name) - ticker_series = ticker_series.rename(stock) - prices = pd.concat([prices, ticker_series], axis=1, sort=True) - - return prices - - else: - error_message = ("Pandas is not installed, but .get_ticker_price() was " - "called with fmt=pandas. In order to install tiingo with " - "pandas, reinstall with pandas as an optional dependency. \n" - "Install tiingo with pandas dependency: \'pip install tiingo[pandas]\'\n" - "Alternatively, just install pandas: pip install pandas.") - raise InstallPandasException(error_message) - - -### End of Modifications (GenusGeoff) - - # NEWS FEEDS - # tiingo/news - def get_news(self, tickers=[], tags=[], sources=[], startDate=None, - endDate=None, limit=100, offset=0, sortBy="publishedDate", - onlyWithTickers=False, - fmt='json'): - """Return list of news articles matching given search terms - https://api.tiingo.com/docs/tiingo/news - - # Dates are in YYYY-MM-DD Format. - - Args: - tickers [string] : List of unique Stock Tickers to search - tags [string] : List of topics tagged by Tiingo Algorithms - sources [string]: List of base urls to include as news sources - startDate, endDate [date]: Boundaries of news search window - limit (int): Max results returned. Default 100, max 1000 - offset (int): Search results offset, used for paginating - sortBy (string): "publishedDate" OR "crawlDate", descending - onlyWithTickers (bool): If true, only links with tagged tickers will return. - """ - url = "tiingo/news" - params = { - 'limit': limit, - 'offset': offset, - 'sortBy': sortBy, - 'tickers': tickers, - 'source': (",").join(sources) if sources else None, - 'tags': tags, - 'startDate': startDate, - 'endDate': endDate, - 'onlyWithTickers': onlyWithTickers - } - response = self._request('GET', url, params=params) - data = response.json() - if fmt == 'json': - return data - elif fmt == 'object': - return [dict_to_object(item, "NewsArticle") for item in data] - - def get_bulk_news(self, file_id=None, fmt='json'): - """Only available to institutional clients. - If ID is NOT provided, return array of available file_ids. - If ID is provided, provides URL which you can use to download your - file, as well as some metadata about that file. - """ - if file_id: - url = "tiingo/news/bulk_download/{}".format(file_id) - else: - url = "tiingo/news/bulk_download" - - response = self._request('GET', url) - data = response.json() - if fmt == 'json': - return data - elif fmt == 'object': - return dict_to_object(data, "BulkNews") - - # Crypto - # tiingo/crypto - def get_crypto_top_of_book(self, tickers=[], exchanges=[], - includeRawExchangeData=False, convertCurrency=None): - url = 'tiingo/crypto/top' - params = { - 'tickers': ','.join(tickers) - } - - if len(exchanges): - params['exchanges'] = ','.join(exchanges) - if includeRawExchangeData is True: - params['includeRawExchangeData'] = True - if convertCurrency: - params['convertCurrency'] = convertCurrency - - response = self._request('GET', url, params=params) - return response.json() - - def get_crypto_price_history(self, tickers=[], baseCurrency=None, - startDate=None, endDate=None, exchanges=[], - consolidateBaseCurrency=False, includeRawExchangeData=False, - resampleFreq=None, convertCurrency=None): - url = 'tiingo/crypto/prices' - params = { - 'tickers': ','.join(tickers) - } - - if startDate: - params['startDate'] = startDate - if endDate: - params['endDate'] = endDate - if len(exchanges): - params['exchanges'] = ','.join(exchanges) - if consolidateBaseCurrency is True: - params['consolidateBaseCurrency'] = ','.join(consolidateBaseCurrency) - if includeRawExchangeData is True: - params['includeRawExchangeData'] = includeRawExchangeData - if resampleFreq: - params['resampleFreq'] = resampleFreq - if convertCurrency: - params['convertCurrency'] = convertCurrency - - response = self._request('GET', url, params=params) - return response.json() - - def get_crypto_metadata(self, tickers=[], fmt='json'): - url = 'tiingo/crypto' - - params = { - 'tickers': ','.join(tickers), - 'format': fmt, - } - - response = self._request('GET', url, params=params) - if fmt == 'csv': - return response.content.decode("utf-8") - else: - return response.json() - - # FUNDAMENTAL DEFINITIONS - # tiingo/fundamentals/definitions - def get_fundamentals_definitions(self, tickers=[], fmt='json'): - """Return definitions for fundamentals for specified tickers - https://api.tiingo.com/documentation/fundamentals - - Args: - tickers [string] : optional, either list or string - fmt (string): 'csv' or 'json' - """ - url = "tiingo/fundamentals/definitions" - params = { - 'tickers': tickers, - 'format': fmt - } - response = self._request('GET', url, params=params) - if fmt == 'json': - return response.json() - elif fmt == 'csv': - return response.content.decode("utf-8") - - # FUNDAMENTAL DAILY - # tiingo/fundamentals//daily - def get_fundamentals_daily(self, ticker, fmt='json', - startDate=None, endDate=None): - """Returns metrics which rely on daily price-updates - https://api.tiingo.com/documentation/fundamentals - - # Dates are in YYYY-MM-DD Format. - - Args: - tickers [string] : List of unique Stock Tickers to search - startDate, endDate [date]: Boundaries of search window - fmt (string): 'csv' or 'json' - """ - url = 'tiingo/fundamentals/{}/daily'.format(ticker) - params = { - 'startDate': startDate, - 'endDate': endDate, - 'format': fmt - } - response = self._request('GET', url, params=params) - if fmt == 'json': - return response.json() - elif fmt == 'csv': - return response.content.decode("utf-8") - - # FUNDAMENTAL STATEMENTS - # tiingo/fundamentals//statements - def get_fundamentals_statements(self, ticker, asReported=False, fmt='json', - startDate=None, endDate=None): - """Returns data that is extracted from quarterly and annual statements. - https://api.tiingo.com/documentation/fundamentals - - # Dates are in YYYY-MM-DD Format. - - Args: - tickers [string] : List of unique Stock Tickers to search - startDate, endDate [date]: Boundaries of search window - asReported [bool]: get most-recent data (False) or data \ - as it was reported on the release-date - (True) - fmt (string): 'csv' or 'json' - """ - if asReported: - asReported = 'true' - else: - asReported = 'false' - - url = 'tiingo/fundamentals/{}/statements'.format(ticker) - params = { - 'startDate': startDate, - 'endDate': endDate, - 'asReported': asReported, - 'format': fmt - } - response = self._request('GET', url, params=params) - if fmt == 'json': - return response.json() - elif fmt == 'csv': - return response.content.decode("utf-8") +# -*- coding: utf-8 -*- + +from collections import namedtuple +import csv +import json +import os +import re +import sys +import pkg_resources +from zipfile import ZipFile + +import requests + +from tiingo.restclient import RestClient +from tiingo.exceptions import ( + InstallPandasException, + APIColumnNameError, + InvalidFrequencyError, + MissingRequiredArgumentError) + +try: + import pandas as pd + pandas_is_installed = True +except ImportError: + pandas_is_installed = False + +VERSION = pkg_resources.get_distribution("tiingo").version + + +# These methods enable python 2 + 3 compatibility. +def get_zipfile_from_response(response): + if sys.version_info < (3, 0): # python 2 + from StringIO import StringIO as Buffer + else: # python 3 + from io import BytesIO as Buffer + buffered = Buffer(response.content) + return ZipFile(buffered) + + +def get_buffer_from_zipfile(zipfile, filename): + if sys.version_info < (3, 0): # python 2 + from StringIO import StringIO + return StringIO(zipfile.read(filename)) + else: # python 3 + # Source: + # https://stackoverflow.com/questions/5627954/py3k-how-do-you-read-a-file-inside-a-zip-file-as-text-not-bytes + from io import (TextIOWrapper, BytesIO) + return TextIOWrapper(BytesIO(zipfile.read(filename))) + + +def dict_to_object(item, object_name): + """Converts a python dict to a namedtuple, saving memory.""" + fields = item.keys() + values = item.values() + return json.loads(json.dumps(item), + object_hook=lambda d: + namedtuple(object_name, fields)(*values)) + + +class TiingoClient(RestClient): + """Class for managing interactions with the Tiingo REST API + + Supply API Key via Environment Variable TIINGO_API_KEY + or via the Config Object + """ + + def __init__(self, *args, **kwargs): + super(TiingoClient, self).__init__(*args, **kwargs) + self._base_url = "https://api.tiingo.com" + + try: + api_key = self._config['api_key'] + except KeyError: + api_key = os.environ.get('TIINGO_API_KEY') + self._api_key = api_key + + if not(api_key): + raise RuntimeError("Tiingo API Key not provided. Please provide" + " via environment variable or config argument.") + + self._headers = { + 'Authorization': "Token {}".format(api_key), + 'Content-Type': 'application/json', + 'User-Agent': 'tiingo-python-client {}'.format(VERSION) + } + + self._frequency_pattern = re.compile('^[0-9]+(min|hour)$', re.IGNORECASE) + + def __repr__(self): + return ''.format(self._base_url) + + def _is_eod_frequency(self,frequency): + return frequency.lower() in ['daily', 'weekly', 'monthly', 'annually'] + + # TICKER PRICE ENDPOINTS + # https://api.tiingo.com/docs/tiingo/daily + def list_tickers(self, assetTypes=[]): + """Return a list of dicts of metadata tickers for all supported tickers + of the specified asset type, as well as metadata about each ticker. + This includes supported date range, the exchange the ticker is traded + on, and the currency the stock is traded on. + Tickers for unrelated products are omitted. + https://apimedia.tiingo.com/docs/tiingo/daily/supported_tickers.zip + """ + listing_file_url = "https://apimedia.tiingo.com/docs/tiingo/daily/supported_tickers.zip" + response = requests.get(listing_file_url) + zipdata = get_zipfile_from_response(response) + raw_csv = get_buffer_from_zipfile(zipdata, 'supported_tickers.csv') + reader = csv.DictReader(raw_csv) + + if not len(assetTypes): + return [row for row in reader] + + assetTypesSet = set(assetTypes) + return [row for row in reader + if row.get('assetType') in assetTypesSet] + + def list_stock_tickers(self): + return self.list_tickers(['Stock']) + + def list_etf_tickers(self): + return self.list_tickers(['ETF']) + + def list_fund_tickers(self): + return self.list_tickers(['Mutual Fund']) + + def get_ticker_metadata(self, ticker, fmt='json'): + """Return metadata for 1 ticker + Use TiingoClient.list_tickers() to get available options + + Args: + ticker (str) : Unique identifier for stock + """ + url = "tiingo/daily/{}".format(ticker) + response = self._request('GET', url) + data = response.json() + if fmt == 'json': + return data + elif fmt == 'object': + return dict_to_object(data, "Ticker") + + def _invalid_frequency(self, frequency): + """ + Check to see that frequency was specified correctly + :param frequency (string): frequency string + :return (boolean): + """ + is_valid = self._is_eod_frequency(frequency) or re.match(self._frequency_pattern, frequency) + return not is_valid + + def _get_url(self, ticker, frequency): + """ + Return url based on frequency. Daily, weekly, or yearly use Tiingo + EOD api; anything less than daily uses the iex intraday api. + :param ticker (string): ticker to be embedded in the url + :param frequency (string): valid frequency per Tiingo api + :return (string): url + """ + if self._invalid_frequency(frequency): + etext = ("Error: {} is an invalid frequency. Check Tiingo API documentation " + "for valid EOD or intraday frequency format.") + raise InvalidFrequencyError(etext.format(frequency)) + else: + if self._is_eod_frequency(frequency): + return "tiingo/daily/{}/prices".format(ticker) + else: + return "iex/{}/prices".format(ticker) + + def _request_pandas(self, ticker, metric_name, params): + """ + Return data for ticker as a pandas.DataFrame if metric_name is not + specified or as a pandas.Series if metric_name is specified. + + :param ticker (string): ticker to be requested + :param params (dict): a dictionary containing valid resampleFreq + and format strings per the Tiingo api + :param metric_name (string): Optional parameter specifying metric to be returned for each + ticker. In the event of a single ticker, this is optional and if not specified + all of the available data will be returned. In the event of a list of tickers, + this parameter is required. + """ + url = self._get_url(ticker, params['resampleFreq']) + response = self._request('GET', url, params=params) + if params['format'] == 'csv': + if sys.version_info < (3, 0): # python 2 + from StringIO import StringIO + else: # python 3 + from io import StringIO + + df = pd.read_csv(StringIO(response.content.decode('utf-8'))) + else: + df = pd.DataFrame(response.json()) + + df.set_index('date', inplace=True) + + if metric_name is not None: + prices = df[metric_name] + else: + prices = df + + prices.index = pd.to_datetime(prices.index) + + # Localize to UTC to ensure equivalence between data returned in json format and + # csv format. Tiingo daily data requested in csv format does not include a timezone. + if prices.index.tz is None: + prices.index = prices.index.tz_localize('UTC') + + return prices + + def get_ticker_price(self, ticker, + startDate=None, endDate=None, + fmt='json', frequency='daily'): + """By default, return latest EOD Composite Price for a stock ticker. + On average, each feed contains 3 data sources. + + Supported tickers + Available Day Ranges are here: + https://apimedia.tiingo.com/docs/tiingo/daily/supported_tickers.zip + + Args: + ticker (string): Unique identifier for stock ticker + startDate (string): Start of ticker range in YYYY-MM-DD format + endDate (string): End of ticker range in YYYY-MM-DD format + fmt (string): 'csv' or 'json' + frequency (string): Resample frequency + """ + url = self._get_url(ticker, frequency) + params = { + 'format': fmt if fmt != "object" else 'json', # conversion local + 'resampleFreq': frequency + } + + if startDate: + params['startDate'] = startDate + if endDate: + params['endDate'] = endDate + + # TODO: evaluate whether to stream CSV to cache on disk, or + # load as array in memory, or just pass plain text + response = self._request('GET', url, params=params) + if fmt == "json": + return response.json() + elif fmt == "object": + data = response.json() + return [dict_to_object(item, "TickerPrice") for item in data] + else: + return response.content.decode("utf-8") + + def get_dataframe(self, tickers, + startDate=None, endDate=None, metric_name=None, + frequency='daily', fmt='json'): + + """ Return a pandas.DataFrame of historical prices for one or more ticker symbols. + + By default, return latest EOD Composite Price for a list of stock tickers. + On average, each feed contains 3 data sources. + + Supported tickers + Available Day Ranges are here: + https://apimedia.tiingo.com/docs/tiingo/daily/supported_tickers.zip + or from the TiingoClient.list_tickers() method. + + Args: + tickers (string/list): One or more unique identifiers for a stock ticker. + startDate (string): Start of ticker range in YYYY-MM-DD format. + endDate (string): End of ticker range in YYYY-MM-DD format. + metric_name (string): Optional parameter specifying metric to be returned for each + ticker. In the event of a single ticker, this is optional and if not specified + all of the available data will be returned. In the event of a list of tickers, + this parameter is required. + frequency (string): Resample frequency (defaults to daily). + fmt (string): 'csv' or 'json' + """ + + valid_columns = {'open', 'high', 'low', 'close', 'volume', 'adjOpen', 'adjHigh', 'adjLow', + 'adjClose', 'adjVolume', 'divCash', 'splitFactor'} + + if metric_name is not None and metric_name not in valid_columns: + raise APIColumnNameError('Valid data items are: ' + str(valid_columns)) + + if metric_name is None and isinstance(tickers, list): + raise MissingRequiredArgumentError("""When tickers is provided as a list, metric_name is a required argument. + Please provide a metric_name, or call this method with one ticker at a time.""") + + params = { + 'format': fmt, + 'resampleFreq': frequency + } + if startDate: + params['startDate'] = startDate + if endDate: + params['endDate'] = endDate + + if pandas_is_installed: + if type(tickers) is str: + prices = self._request_pandas( + ticker=tickers, params=params, metric_name=metric_name) + else: + prices = pd.DataFrame() + for stock in tickers: + ticker_series = self._request_pandas( + ticker=stock, params=params, metric_name=metric_name) + ticker_series = ticker_series.rename(stock) + prices = pd.concat([prices, ticker_series], axis=1, sort=True) + + return prices + + else: + error_message = ("Pandas is not installed, but .get_ticker_price() was " + "called with fmt=pandas. In order to install tiingo with " + "pandas, reinstall with pandas as an optional dependency. \n" + "Install tiingo with pandas dependency: \'pip install tiingo[pandas]\'\n" + "Alternatively, just install pandas: pip install pandas.") + raise InstallPandasException(error_message) + + # NEWS FEEDS + # tiingo/news + def get_news(self, tickers=[], tags=[], sources=[], startDate=None, + endDate=None, limit=100, offset=0, sortBy="publishedDate", + onlyWithTickers=False, + fmt='json'): + """Return list of news articles matching given search terms + https://api.tiingo.com/docs/tiingo/news + + # Dates are in YYYY-MM-DD Format. + + Args: + tickers [string] : List of unique Stock Tickers to search + tags [string] : List of topics tagged by Tiingo Algorithms + sources [string]: List of base urls to include as news sources + startDate, endDate [date]: Boundaries of news search window + limit (int): Max results returned. Default 100, max 1000 + offset (int): Search results offset, used for paginating + sortBy (string): "publishedDate" OR "crawlDate", descending + onlyWithTickers (bool): If true, only links with tagged tickers will return. + """ + url = "tiingo/news" + params = { + 'limit': limit, + 'offset': offset, + 'sortBy': sortBy, + 'tickers': tickers, + 'source': (",").join(sources) if sources else None, + 'tags': tags, + 'startDate': startDate, + 'endDate': endDate, + 'onlyWithTickers': onlyWithTickers + } + response = self._request('GET', url, params=params) + data = response.json() + if fmt == 'json': + return data + elif fmt == 'object': + return [dict_to_object(item, "NewsArticle") for item in data] + + def get_bulk_news(self, file_id=None, fmt='json'): + """Only available to institutional clients. + If ID is NOT provided, return array of available file_ids. + If ID is provided, provides URL which you can use to download your + file, as well as some metadata about that file. + """ + if file_id: + url = "tiingo/news/bulk_download/{}".format(file_id) + else: + url = "tiingo/news/bulk_download" + + response = self._request('GET', url) + data = response.json() + if fmt == 'json': + return data + elif fmt == 'object': + return dict_to_object(data, "BulkNews") + + # Crypto + # tiingo/crypto + def get_crypto_top_of_book(self, tickers=[], exchanges=[], + includeRawExchangeData=False, convertCurrency=None): + url = 'tiingo/crypto/top' + params = { + 'tickers': ','.join(tickers) + } + + if len(exchanges): + params['exchanges'] = ','.join(exchanges) + if includeRawExchangeData is True: + params['includeRawExchangeData'] = True + if convertCurrency: + params['convertCurrency'] = convertCurrency + + response = self._request('GET', url, params=params) + return response.json() + + def get_crypto_price_history(self, tickers=[], baseCurrency=None, + startDate=None, endDate=None, exchanges=[], + consolidateBaseCurrency=False, includeRawExchangeData=False, + resampleFreq=None, convertCurrency=None): + url = 'tiingo/crypto/prices' + params = { + 'tickers': ','.join(tickers) + } + + if startDate: + params['startDate'] = startDate + if endDate: + params['endDate'] = endDate + if len(exchanges): + params['exchanges'] = ','.join(exchanges) + if consolidateBaseCurrency is True: + params['consolidateBaseCurrency'] = ','.join(consolidateBaseCurrency) + if includeRawExchangeData is True: + params['includeRawExchangeData'] = includeRawExchangeData + if resampleFreq: + params['resampleFreq'] = resampleFreq + if convertCurrency: + params['convertCurrency'] = convertCurrency + + response = self._request('GET', url, params=params) + return response.json() + + def get_crypto_metadata(self, tickers=[], fmt='json'): + url = 'tiingo/crypto' + + params = { + 'tickers': ','.join(tickers), + 'format': fmt, + } + + response = self._request('GET', url, params=params) + if fmt == 'csv': + return response.content.decode("utf-8") + else: + return response.json() + + # FUNDAMENTAL DEFINITIONS + # tiingo/fundamentals/definitions + def get_fundamentals_definitions(self, tickers=[], fmt='json'): + """Return definitions for fundamentals for specified tickers + https://api.tiingo.com/documentation/fundamentals + + Args: + tickers [string] : optional, either list or string + fmt (string): 'csv' or 'json' + """ + url = "tiingo/fundamentals/definitions" + params = { + 'tickers': tickers, + 'format': fmt + } + response = self._request('GET', url, params=params) + if fmt == 'json': + return response.json() + elif fmt == 'csv': + return response.content.decode("utf-8") + + # FUNDAMENTAL DAILY + # tiingo/fundamentals//daily + def get_fundamentals_daily(self, ticker, fmt='json', + startDate=None, endDate=None): + """Returns metrics which rely on daily price-updates + https://api.tiingo.com/documentation/fundamentals + + # Dates are in YYYY-MM-DD Format. + + Args: + tickers [string] : List of unique Stock Tickers to search + startDate, endDate [date]: Boundaries of search window + fmt (string): 'csv' or 'json' + """ + url = 'tiingo/fundamentals/{}/daily'.format(ticker) + params = { + 'startDate': startDate, + 'endDate': endDate, + 'format': fmt + } + response = self._request('GET', url, params=params) + if fmt == 'json': + return response.json() + elif fmt == 'csv': + return response.content.decode("utf-8") + + # FUNDAMENTAL STATEMENTS + # tiingo/fundamentals//statements + def get_fundamentals_statements(self, ticker, asReported=False, fmt='json', + startDate=None, endDate=None): + """Returns data that is extracted from quarterly and annual statements. + https://api.tiingo.com/documentation/fundamentals + + # Dates are in YYYY-MM-DD Format. + + Args: + tickers [string] : List of unique Stock Tickers to search + startDate, endDate [date]: Boundaries of search window + asReported [bool]: get most-recent data (False) or data \ + as it was reported on the release-date + (True) + fmt (string): 'csv' or 'json' + """ + if asReported: + asReported = 'true' + else: + asReported = 'false' + + url = 'tiingo/fundamentals/{}/statements'.format(ticker) + params = { + 'startDate': startDate, + 'endDate': endDate, + 'asReported': asReported, + 'format': fmt + } + response = self._request('GET', url, params=params) + if fmt == 'json': + return response.json() + elif fmt == 'csv': + return response.content.decode("utf-8")