From ea6a2986d6f8c8c7c0288989d97e8f84ff9bc285 Mon Sep 17 00:00:00 2001 From: GenusGeoff <58801792+GenusGeoff@users.noreply.github.com> Date: Sat, 27 Feb 2021 18:53:33 -0500 Subject: [PATCH 1/7] Update api.py Added functionality in rough form as requested in "[Enhancement] Add new daily and IEX methods following the Tiingo API more closely #527" from tiingo-python --- tiingo/api.py | 1167 +++++++++++++++++++++++++++---------------------- 1 file changed, 655 insertions(+), 512 deletions(-) diff --git a/tiingo/api.py b/tiingo/api.py index 49702a4..ce6bf51 100644 --- a/tiingo/api.py +++ b/tiingo/api.py @@ -1,512 +1,655 @@ -# -*- 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") +# -*- 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") From 214a03a1efd3e0fb8edaec7aab57a0ddb93df8fc Mon Sep 17 00:00:00 2001 From: GenusGeoff <58801792+GenusGeoff@users.noreply.github.com> Date: Mon, 1 Mar 2021 20:08:52 -0500 Subject: [PATCH 2/7] Update README.rst with suggested modifications Updated with some cryptocurrency examples as requested in #520. I also resolved some capitalization problems in the code, e.g. CLIENT.method rather than client.method as the code example dictates. --- README.rst | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 010b00d..fe12a43 100644 --- a/README.rst +++ b/README.rst @@ -78,7 +78,8 @@ Alternately, you may use a dictionary to customize/authorize your client. # Initialize client = TiingoClient(config) -Now you can use ``TiingoClient`` to make your API calls. (Other parameters are available for each endpoint beyond what is used in the below examples, inspect the docstring for each function for details.). +Now you can use ``TiingoClient`` to make your API calls. (Other parameters are available for each endpoint beyond what is used in the below examples, inspect the docstring +for each function for details.). .. code-block:: python @@ -108,11 +109,11 @@ Now you can use ``TiingoClient`` to make your API calls. (Other parameters are a # Get definitions for fields available in the fundamentals-api, ticker is # optional - definitions = get_fundamentals_definitions('GOOGL') + definitions = client.get_fundamentals_definitions('GOOGL') # Get fundamentals which require daily-updated (like marketCap). A start- # and end-date can be passed. If omited, will get all available data. - fundamentals_daily = CLIENT.get_fundamentals_daily('GOOGL', + fundamentals_daily = client.get_fundamentals_daily('GOOGL', startDate='2020-01-01', endDate='2020-12-31') @@ -120,7 +121,7 @@ Now you can use ``TiingoClient`` to make your API calls. (Other parameters are a # daily-fundamentals. asReported can be set to get the data exactly like # it was reported to SEC. Set to False if you want to get data containing # corrections - fundamentals_stmnts = CLIENT.get_fundamentals_statements('GOOGL', + fundamentals_stmnts = client.get_fundamentals_statements('GOOGL', startDate='2020-01-01', endDate='2020-12-31', asReported=True) @@ -152,7 +153,24 @@ To receive results in ``pandas`` format, use the ``get_dataframe()`` method: endDate='2018-05-31') -You can specify any of the end of day frequencies (daily, weekly, monthly, and annually) or any intraday frequency for both the ``get_ticker_price`` and ``get_dataframe`` methods. Weekly frequencies resample to the end of day on Friday, monthly frequencies resample to the last day of the month, and annually frequencies resample to the end of day on 12-31 of each year. The intraday frequencies are specified using an integer followed by "Min" or "Hour", for example "30Min" or "1Hour". +You can specify any of the end of day frequencies (daily, weekly, monthly, and annually) or any intraday frequency for both the ``get_ticker_price`` and ``get_dataframe`` +methods. Weekly frequencies resample to the end of day on Friday, monthly frequencies resample to the last day of the month, and annually frequencies resample to the end of +day on 12-31 of each year. The intraday frequencies are specified using an integer followed by "Min" or "Hour", for example "30Min" or "1Hour". + +.. code-block:: python + +# You can obtain cryptocurrency metadata using the following method. +client.get_crypto_metadata(['BTCUSD'], fmt='json') + +# You can obtain top-of-book cryptocurrency quotes from the ``get_crypto_top_of_book()`` method. + +# NOTE: Crypto symbol MUST be encapsulated in brackets as a Python list! +crypto_price = client.get_crypto_top_of_book(['BTCUSD'])`` + +# You can obtain historical Cryptocurrency price quotes from the get_crypto_price_history() method. + +# NOTE: Crypto symbol MUST be encapsulated in brackets as a Python list! +client.get_crypto_price_history(tickers = ['BTCUSD'], startDate='2020-12-2', endDate='2020-12-3', resampleFreq='1Hour') Further Docs ------------- From f2b3b9c99d53ee088bf6175a6c1a41f54637e3b1 Mon Sep 17 00:00:00 2001 From: GenusGeoff <58801792+GenusGeoff@users.noreply.github.com> Date: Mon, 1 Mar 2021 20:10:25 -0500 Subject: [PATCH 3/7] Update README.rst Trying to get the formatting correct. --- README.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index fe12a43..290ffd4 100644 --- a/README.rst +++ b/README.rst @@ -159,18 +159,18 @@ day on 12-31 of each year. The intraday frequencies are specified using an inte .. code-block:: python -# You can obtain cryptocurrency metadata using the following method. -client.get_crypto_metadata(['BTCUSD'], fmt='json') + # You can obtain cryptocurrency metadata using the following method. + client.get_crypto_metadata(['BTCUSD'], fmt='json') -# You can obtain top-of-book cryptocurrency quotes from the ``get_crypto_top_of_book()`` method. +You can obtain top-of-book cryptocurrency quotes from the ``get_crypto_top_of_book()`` method. -# NOTE: Crypto symbol MUST be encapsulated in brackets as a Python list! -crypto_price = client.get_crypto_top_of_book(['BTCUSD'])`` + # NOTE: Crypto symbol MUST be encapsulated in brackets as a Python list! + crypto_price = client.get_crypto_top_of_book(['BTCUSD'])`` -# You can obtain historical Cryptocurrency price quotes from the get_crypto_price_history() method. +You can obtain historical Cryptocurrency price quotes from the get_crypto_price_history() method. -# NOTE: Crypto symbol MUST be encapsulated in brackets as a Python list! -client.get_crypto_price_history(tickers = ['BTCUSD'], startDate='2020-12-2', endDate='2020-12-3', resampleFreq='1Hour') + # NOTE: Crypto symbol MUST be encapsulated in brackets as a Python list! + client.get_crypto_price_history(tickers = ['BTCUSD'], startDate='2020-12-2', endDate='2020-12-3', resampleFreq='1Hour') Further Docs ------------- From 57514cd637c8ff0936e98ec8750a86530a282bd8 Mon Sep 17 00:00:00 2001 From: GenusGeoff <58801792+GenusGeoff@users.noreply.github.com> Date: Mon, 1 Mar 2021 20:12:23 -0500 Subject: [PATCH 4/7] Update README.rst Trying to work out the formatting --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 290ffd4..f6d2405 100644 --- a/README.rst +++ b/README.rst @@ -162,14 +162,14 @@ day on 12-31 of each year. The intraday frequencies are specified using an inte # You can obtain cryptocurrency metadata using the following method. client.get_crypto_metadata(['BTCUSD'], fmt='json') -You can obtain top-of-book cryptocurrency quotes from the ``get_crypto_top_of_book()`` method. - + #You can obtain top-of-book cryptocurrency quotes from the ``get_crypto_top_of_book()`` method. # NOTE: Crypto symbol MUST be encapsulated in brackets as a Python list! + crypto_price = client.get_crypto_top_of_book(['BTCUSD'])`` -You can obtain historical Cryptocurrency price quotes from the get_crypto_price_history() method. - + # You can obtain historical Cryptocurrency price quotes from the get_crypto_price_history() method. # NOTE: Crypto symbol MUST be encapsulated in brackets as a Python list! + client.get_crypto_price_history(tickers = ['BTCUSD'], startDate='2020-12-2', endDate='2020-12-3', resampleFreq='1Hour') Further Docs From 5ec3dcea3ba2f2f7d503dac974c6a95ccc66738e Mon Sep 17 00:00:00 2001 From: GenusGeoff <58801792+GenusGeoff@users.noreply.github.com> Date: Mon, 1 Mar 2021 20:14:27 -0500 Subject: [PATCH 5/7] Update README.rst Got the formatting in order --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index f6d2405..4336521 100644 --- a/README.rst +++ b/README.rst @@ -160,6 +160,8 @@ day on 12-31 of each year. The intraday frequencies are specified using an inte .. code-block:: python # You can obtain cryptocurrency metadata using the following method. + # NOTE: Crypto symbol MUST be encapsulated in brackets as a Python list! + client.get_crypto_metadata(['BTCUSD'], fmt='json') #You can obtain top-of-book cryptocurrency quotes from the ``get_crypto_top_of_book()`` method. From a43b19230774c43d4b1271aefe4c4a9a381e7e2d Mon Sep 17 00:00:00 2001 From: GenusGeoff <58801792+GenusGeoff@users.noreply.github.com> Date: Mon, 1 Mar 2021 20:20:02 -0500 Subject: [PATCH 6/7] Update README.rst Fixing formatting --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 4336521..4931310 100644 --- a/README.rst +++ b/README.rst @@ -172,7 +172,8 @@ day on 12-31 of each year. The intraday frequencies are specified using an inte # You can obtain historical Cryptocurrency price quotes from the get_crypto_price_history() method. # NOTE: Crypto symbol MUST be encapsulated in brackets as a Python list! - client.get_crypto_price_history(tickers = ['BTCUSD'], startDate='2020-12-2', endDate='2020-12-3', resampleFreq='1Hour') + client.get_crypto_price_history(tickers = ['BTCUSD'], startDate='2020-12-2', + endDate='2020-12-3', resampleFreq='1Hour') Further Docs ------------- From 969cbca4124b0ad366d476ea55a31e55677aea7c Mon Sep 17 00:00:00 2001 From: GenusGeoff <58801792+GenusGeoff@users.noreply.github.com> Date: Wed, 17 Mar 2021 06:32:36 -0400 Subject: [PATCH 7/7] Rebasing api.py --- tiingo/api.py | 1167 ++++++++++++++++++++++--------------------------- 1 file changed, 512 insertions(+), 655 deletions(-) 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")