From e3f26f609b0003ec373116227c7cae79952b465c Mon Sep 17 00:00:00 2001 From: Davis Thames Date: Wed, 9 May 2018 22:26:18 -0500 Subject: [PATCH 1/9] issue#85 initial commit --- setup.py | 1 + tests/test_tiingo_pandas.py | 57 ++++++++++++++++++++++++++++ tiingo/api.py | 76 ++++++++++++++++++++++++++++++++++++- tools/api_key_tool.py | 21 +++++++--- 4 files changed, 148 insertions(+), 7 deletions(-) create mode 100644 tests/test_tiingo_pandas.py diff --git a/setup.py b/setup.py index 71a43d3..36fc155 100644 --- a/setup.py +++ b/setup.py @@ -57,6 +57,7 @@ setup( packages=find_packages(include=[NAME]), include_package_data=True, install_requires=requirements, + extras_require={'pandas': ['pandas>=0.18']}, license="MIT license", zip_safe=False, keywords=['tiingo', 'finance', 'stocks'], diff --git a/tests/test_tiingo_pandas.py b/tests/test_tiingo_pandas.py new file mode 100644 index 0000000..76d415f --- /dev/null +++ b/tests/test_tiingo_pandas.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +"""Unit tests for pandas functionality in tiingo""" + +import vcr +from unittest import TestCase +from tiingo import TiingoClient +from tiingo.api import APIColumnNameError +try: + import pandas as pd + pandas_is_installed = True +except ImportError: + pandas_is_installed = False + + +class TestTiingoWithPython(TestCase): + + def setUp(self): + if pandas_is_installed: + self._client = TiingoClient() + else: + self.skipTest("test_tiingo_pandas: Pandas not installed.") + + @vcr.use_cassette('tests/fixtures/ticker_price_pandas_weekly.yaml') + def test_return_pandas_format(self): + """Test that valid pandas format is returned when specified""" + prices = self._client.get_dataframe("GOOGL", startDate='2018-01-05', + endDate='2018-01-19', frequency='weekly') + self.assertTrue(isinstance(prices, pd.DataFrame)) + + @vcr.use_cassette('tests/fixtures/ticker_price_pandas_weekly_multiple_tickers.yaml') + def test_return_pandas_format_multiple(self): + """Test that valid pandas format is returned when specified""" + tickers = ["GOOGL", "AAPL"] + prices = self._client.get_dataframe(tickers, startDate='2018-01-05', + endDate='2018-01-19', metric_name='adjClose', frequency='weekly') + self.assertTrue(isinstance(prices, pd.DataFrame)) + assert prices['GOOGL'].loc['2018-01-05'] == 1110.29 + self.assertAlmostEqual(prices['AAPL'].loc['2018-01-19'], 178.54, 2) + + @vcr.use_cassette('tests/fixtures/ticker_price_pandas_daily.yaml') + def test_return_pandas_daily(self): + """Test that valid pandas format is returned when specified""" + prices = self._client.get_dataframe("GOOGL", startDate='2018-01-05', + endDate='2018-01-19', frequency='daily') + self.assertTrue(isinstance(prices, pd.DataFrame)) + assert prices['adjClose'].loc['2018-01-05'] == 1110.29 + + def test_column_error(self): + with self.assertRaises(APIColumnNameError): + self._client.get_dataframe(['GOOGL', 'AAPL'], startDate='2018-01-05', + endDate='2018-01-19', metric_name='xopen', frequency='weekly') + @vcr.use_cassette('tests/fixtures/ticker_price_pandas_single.yaml') + def test_pandas_edge_case(self): + """Test single price/date being returned as a frame""" + prices = self._client.get_dataframe("GOOGL") + assert len(prices) == 1 + assert len(prices.index) == 1 diff --git a/tiingo/api.py b/tiingo/api.py index 697fd47..d3698ad 100644 --- a/tiingo/api.py +++ b/tiingo/api.py @@ -7,10 +7,13 @@ import csv import json from collections import namedtuple from zipfile import ZipFile - - from tiingo.restclient import RestClient import requests +try: + import pandas as pd + pandas_is_installed = True +except ImportError: + pandas_is_installed = False VERSION = pkg_resources.get_distribution("tiingo").version @@ -44,6 +47,13 @@ def dict_to_object(item, object_name): object_hook=lambda d: namedtuple(object_name, fields)(*values)) +class InstallPandasException(Exception): + pass + + +class APIColumnNameError(Exception): + pass + class TiingoClient(RestClient): """Class for managing interactions with the Tiingo REST API @@ -60,6 +70,7 @@ class TiingoClient(RestClient): 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" @@ -155,6 +166,67 @@ class TiingoClient(RestClient): else: return response.content.decode("utf-8") + def _build_url(self, stock, startDate, endDate, frequency): + url = "https://api.tiingo.com/tiingo/" + url += "daily/{}/prices?".format(stock) + if startDate is not None and endDate is None: + url += "&startDate={}".format(startDate) + if startDate is not None and endDate is not None: + url += "&startDate={}&endDate={}".format(startDate, endDate) + url += "&format=json&resampleFreq={}&token={}".format(frequency, self._api_key) + return url + + def get_dataframe(self, tickers, + startDate=None, endDate=None, metric_name='adjClose', frequency='daily'): + + """ 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 + + 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). + """ + + valid_columns = ['open', 'high', 'low', 'close', 'volume', 'adjOpen', 'adjHigh', 'adjLow', + 'adjClose', 'adjVolume', 'divCash', 'splitFactor'] + if pandas_is_installed: + prices = pd.DataFrame() + if type(tickers) is str: + stock = tickers + url = self._build_url(stock, startDate, endDate, frequency) + prices = pd.read_json(url) + prices.index = prices['date'] + del(prices['date']) + else: + if metric_name not in valid_columns: + raise APIColumnNameError('Valid data items are: '+str(valid_columns)) + for stock in tickers: + url = self._build_url(stock, startDate, endDate, frequency) + df = pd.read_json(url) + df.index = df['date'] + df.rename(index=str, columns={metric_name: stock}, inplace=True) + prices = pd.concat([prices, df[stock]], axis=1) + prices.index = pd.to_datetime(prices.index) + 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, diff --git a/tools/api_key_tool.py b/tools/api_key_tool.py index 3d92246..dd72f10 100755 --- a/tools/api_key_tool.py +++ b/tools/api_key_tool.py @@ -6,35 +6,46 @@ import re import argparse fixtures_directory = 'tests/fixtures/' + +# restclient api header configuration zero_api_regex = r'(\[Token )0{40}(\])' real_api_regex = r'(\[Token ).{40}(\])' zero_token_string = '[Token ' + 40 * '0' + ']' +# pandas json api call configuration +pd_real_api_regex = r'&token=.{40}' +pd_zero_api_regex = r'&token=0{40}' +pd_zero_token_string = '&token=' + 40 * '0' -def has_api_key(file): + +def has_api_key(file_name): """ Detect whether the file contains an api key in the Token object that is not 40*'0'. See issue #86. :param file: path-to-file to check :return: boolean """ - f = open(file, 'r') + f = open(file_name, 'r') text = f.read() if re.search(real_api_regex, text) is not None and \ re.search(zero_api_regex, text) is None: return True + elif re.search(pd_real_api_regex, text) is not None and \ + re.search(pd_zero_api_regex, text) is None: + return True return False -def remove_api_key(file): +def remove_api_key(file_name): """ Change the api key in the Token object to 40*'0'. See issue #86. :param file: path-to-file to change """ - with open(file, 'r') as fp: + with open(file_name, 'r') as fp: text = fp.read() text = re.sub(real_api_regex, zero_token_string, text) - with open(file, 'w') as fp: + text = re.sub(pd_real_api_regex, pd_zero_token_string, text) + with open(file_name, 'w') as fp: fp.write(text) return From 3c369e82080f8e590d146f6afce07050c6c6a40c Mon Sep 17 00:00:00 2001 From: Davis Thames Date: Wed, 9 May 2018 22:27:52 -0500 Subject: [PATCH 2/9] issue#85 add test fixtures --- tests/fixtures/ticker_price_pandas_daily.yaml | 22 ++++++++++ .../fixtures/ticker_price_pandas_single.yaml | 22 ++++++++++ .../fixtures/ticker_price_pandas_weekly.yaml | 22 ++++++++++ ..._price_pandas_weekly_multiple_tickers.yaml | 42 +++++++++++++++++++ 4 files changed, 108 insertions(+) create mode 100644 tests/fixtures/ticker_price_pandas_daily.yaml create mode 100644 tests/fixtures/ticker_price_pandas_single.yaml create mode 100644 tests/fixtures/ticker_price_pandas_weekly.yaml create mode 100644 tests/fixtures/ticker_price_pandas_weekly_multiple_tickers.yaml diff --git a/tests/fixtures/ticker_price_pandas_daily.yaml b/tests/fixtures/ticker_price_pandas_daily.yaml new file mode 100644 index 0000000..510c79b --- /dev/null +++ b/tests/fixtures/ticker_price_pandas_daily.yaml @@ -0,0 +1,22 @@ +interactions: +- request: + body: null + headers: + Connection: [close] + Host: [api.tiingo.com] + User-Agent: [Python-urllib/3.6] + method: GET + uri: https://api.tiingo.com/tiingo/daily/GOOGL/prices?&startDate=2018-01-05&endDate=2018-01-19&format=json&resampleFreq=daily&token=0000000000000000000000000000000000000000 + response: + body: {string: '[{"date":"2018-01-05T00:00:00.000Z","close":1110.29,"high":1113.58,"low":1101.8,"open":1103.45,"volume":1493389,"adjClose":1110.29,"adjHigh":1113.58,"adjLow":1101.8,"adjOpen":1103.45,"adjVolume":1493389,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-08T00:00:00.000Z","close":1114.21,"high":1119.16,"low":1110.0,"open":1111.0,"volume":1148958,"adjClose":1114.21,"adjHigh":1119.16,"adjLow":1110.0,"adjOpen":1111.0,"adjVolume":1148958,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-09T00:00:00.000Z","close":1112.79,"high":1118.44,"low":1108.2,"open":1118.44,"volume":1335995,"adjClose":1112.79,"adjHigh":1118.44,"adjLow":1108.2,"adjOpen":1118.44,"adjVolume":1335995,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-10T00:00:00.000Z","close":1110.14,"high":1112.78,"low":1103.98,"open":1107.0,"volume":1027781,"adjClose":1110.14,"adjHigh":1112.78,"adjLow":1103.98,"adjOpen":1107.0,"adjVolume":1027781,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-11T00:00:00.000Z","close":1112.05,"high":1114.85,"low":1106.48,"open":1112.31,"volume":1102461,"adjClose":1112.05,"adjHigh":1114.85,"adjLow":1106.48,"adjOpen":1112.31,"adjVolume":1102461,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-12T00:00:00.000Z","close":1130.65,"high":1131.3,"low":1108.01,"open":1110.1,"volume":1914460,"adjClose":1130.65,"adjHigh":1131.3,"adjLow":1108.01,"adjOpen":1110.1,"adjVolume":1914460,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-16T00:00:00.000Z","close":1130.7,"high":1148.88,"low":1126.66,"open":1140.31,"volume":1783881,"adjClose":1130.7,"adjHigh":1148.88,"adjLow":1126.66,"adjOpen":1140.31,"adjVolume":1783881,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-17T00:00:00.000Z","close":1139.1,"high":1139.32,"low":1123.49,"open":1136.36,"volume":1353097,"adjClose":1139.1,"adjHigh":1139.32,"adjLow":1123.49,"adjOpen":1136.36,"adjVolume":1353097,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-18T00:00:00.000Z","close":1135.97,"high":1140.59,"low":1124.46,"open":1139.35,"volume":1333633,"adjClose":1135.97,"adjHigh":1140.59,"adjLow":1124.46,"adjOpen":1139.35,"adjVolume":1333633,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-19T00:00:00.000Z","close":1143.5,"high":1143.78,"low":1132.5,"open":1138.03,"volume":1418376,"adjClose":1143.5,"adjHigh":1143.78,"adjLow":1132.5,"adjOpen":1138.03,"adjVolume":1418376,"divCash":0.0,"splitFactor":1.0}]'} + headers: + Allow: ['GET, HEAD, OPTIONS'] + Connection: [close] + Content-Length: ['2349'] + Content-Type: [application/json] + Date: ['Thu, 10 May 2018 03:25:28 GMT'] + Server: [nginx/1.10.1] + Vary: ['Accept, Cookie'] + X-Frame-Options: [SAMEORIGIN] + status: {code: 200, message: OK} +version: 1 diff --git a/tests/fixtures/ticker_price_pandas_single.yaml b/tests/fixtures/ticker_price_pandas_single.yaml new file mode 100644 index 0000000..636eb1f --- /dev/null +++ b/tests/fixtures/ticker_price_pandas_single.yaml @@ -0,0 +1,22 @@ +interactions: +- request: + body: null + headers: + Connection: [close] + Host: [api.tiingo.com] + User-Agent: [Python-urllib/3.6] + method: GET + uri: https://api.tiingo.com/tiingo/daily/GOOGL/prices?&format=json&resampleFreq=daily&token=0000000000000000000000000000000000000000 + response: + body: {string: '[{"adjClose":1088.95,"adjHigh":1094.0,"adjLow":1062.11,"adjOpen":1064.1,"adjVolume":2357979,"close":1088.95,"date":"2018-05-09T00:00:00+00:00","divCash":0.0,"high":1094.0,"low":1062.11,"open":1064.1,"splitFactor":1.0,"volume":2357979}]'} + headers: + Allow: ['GET, HEAD, OPTIONS'] + Connection: [close] + Content-Length: ['235'] + Content-Type: [application/json] + Date: ['Thu, 10 May 2018 03:25:28 GMT'] + Server: [nginx/1.10.1] + Vary: ['Accept, Cookie'] + X-Frame-Options: [SAMEORIGIN] + status: {code: 200, message: OK} +version: 1 diff --git a/tests/fixtures/ticker_price_pandas_weekly.yaml b/tests/fixtures/ticker_price_pandas_weekly.yaml new file mode 100644 index 0000000..0c49b0c --- /dev/null +++ b/tests/fixtures/ticker_price_pandas_weekly.yaml @@ -0,0 +1,22 @@ +interactions: +- request: + body: null + headers: + Connection: [close] + Host: [api.tiingo.com] + User-Agent: [Python-urllib/3.6] + method: GET + uri: https://api.tiingo.com/tiingo/daily/GOOGL/prices?&startDate=2018-01-05&endDate=2018-01-19&format=json&resampleFreq=weekly&token=0000000000000000000000000000000000000000 + response: + body: {string: '[{"date":"2018-01-05T00:00:00.000Z","close":1110.29,"high":1113.58,"low":1053.02,"open":1053.02,"volume":5889084,"adjClose":1110.29,"adjHigh":1113.58,"adjLow":1053.02,"adjOpen":1053.02,"adjVolume":5889084,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-12T00:00:00.000Z","close":1130.65,"high":1131.3,"low":1103.98,"open":1111.0,"volume":6529655,"adjClose":1130.65,"adjHigh":1131.3,"adjLow":1103.98,"adjOpen":1111.0,"adjVolume":6529655,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-19T00:00:00.000Z","close":1135.97,"high":1148.88,"low":1123.49,"open":1140.31,"volume":4470611,"adjClose":1135.97,"adjHigh":1148.88,"adjLow":1123.49,"adjOpen":1140.31,"adjVolume":4470611,"divCash":0.0,"splitFactor":1.0}]'} + headers: + Allow: ['GET, HEAD, OPTIONS'] + Connection: [close] + Content-Length: ['708'] + Content-Type: [application/json] + Date: ['Thu, 10 May 2018 03:25:29 GMT'] + Server: [nginx/1.10.1] + Vary: ['Accept, Cookie'] + X-Frame-Options: [SAMEORIGIN] + status: {code: 200, message: OK} +version: 1 diff --git a/tests/fixtures/ticker_price_pandas_weekly_multiple_tickers.yaml b/tests/fixtures/ticker_price_pandas_weekly_multiple_tickers.yaml new file mode 100644 index 0000000..c531d47 --- /dev/null +++ b/tests/fixtures/ticker_price_pandas_weekly_multiple_tickers.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + body: null + headers: + Connection: [close] + Host: [api.tiingo.com] + User-Agent: [Python-urllib/3.6] + method: GET + uri: https://api.tiingo.com/tiingo/daily/GOOGL/prices?&startDate=2018-01-05&endDate=2018-01-19&format=json&resampleFreq=weekly&token=0000000000000000000000000000000000000000 + response: + body: {string: '[{"date":"2018-01-05T00:00:00.000Z","close":1110.29,"high":1113.58,"low":1053.02,"open":1053.02,"volume":5889084,"adjClose":1110.29,"adjHigh":1113.58,"adjLow":1053.02,"adjOpen":1053.02,"adjVolume":5889084,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-12T00:00:00.000Z","close":1130.65,"high":1131.3,"low":1103.98,"open":1111.0,"volume":6529655,"adjClose":1130.65,"adjHigh":1131.3,"adjLow":1103.98,"adjOpen":1111.0,"adjVolume":6529655,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-19T00:00:00.000Z","close":1135.97,"high":1148.88,"low":1123.49,"open":1140.31,"volume":4470611,"adjClose":1135.97,"adjHigh":1148.88,"adjLow":1123.49,"adjOpen":1140.31,"adjVolume":4470611,"divCash":0.0,"splitFactor":1.0}]'} + headers: + Allow: ['GET, HEAD, OPTIONS'] + Connection: [close] + Content-Length: ['708'] + Content-Type: [application/json] + Date: ['Thu, 10 May 2018 03:25:29 GMT'] + Server: [nginx/1.10.1] + Vary: ['Accept, Cookie'] + X-Frame-Options: [SAMEORIGIN] + status: {code: 200, message: OK} +- request: + body: null + headers: + Connection: [close] + Host: [api.tiingo.com] + User-Agent: [Python-urllib/3.6] + method: GET + uri: https://api.tiingo.com/tiingo/daily/AAPL/prices?&startDate=2018-01-05&endDate=2018-01-19&format=json&resampleFreq=weekly&token=0000000000000000000000000000000000000000 + response: + body: {string: '[{"date":"2018-01-05T00:00:00.000Z","close":175.0,"high":175.37,"low":169.26,"open":170.16,"volume":99095223,"adjClose":174.297949567,"adjHigh":174.6664652318,"adjLow":168.5809768212,"adjOpen":169.4773662761,"adjVolume":99095223,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-12T00:00:00.000Z","close":177.09,"high":177.36,"low":173.0,"open":174.35,"volume":107548622,"adjClose":176.379565079,"adjHigh":176.6484819154,"adjLow":172.3059730005,"adjOpen":173.6505571829,"adjVolume":107548622,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-19T00:00:00.000Z","close":179.26,"high":180.1,"low":175.07,"open":177.9,"volume":92146251,"adjClose":178.5408596536,"adjHigh":179.3774898115,"adjLow":174.3676687468,"adjOpen":177.1863155884,"adjVolume":92146251,"divCash":0.0,"splitFactor":1.0}]'} + headers: + Allow: ['GET, HEAD, OPTIONS'] + Connection: [close] + Content-Length: ['786'] + Content-Type: [application/json] + Date: ['Thu, 10 May 2018 03:25:30 GMT'] + Server: [nginx/1.10.1] + Vary: ['Accept, Cookie'] + X-Frame-Options: [SAMEORIGIN] + status: {code: 200, message: OK} +version: 1 From 1296b2469bca639c48e27ba0ee6dbc88c483cf9d Mon Sep 17 00:00:00 2001 From: Davis Thames Date: Fri, 11 May 2018 11:31:11 -0500 Subject: [PATCH 3/9] Added matrix of tests with and without pandas for Travis --- .travis.yml | 9 +++++++-- tests/test_tiingo_pandas.py | 16 +++++++++++++++- tools/install_pandas.sh | 6 ++++++ 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100755 tools/install_pandas.sh diff --git a/.travis.yml b/.travis.yml index 64f7337..03f5f4b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,10 +6,11 @@ deploy: distributions: sdist bdist_wheel password: secure: VH1rgFV5XD0k3cMZxJwetBnETrFolBaM0U4K9CKQts38LF7f4xA8muY2FFFyeqR7VF2MQhYgGMcaAQQx/gRY+7AdSnqMYvxZ7p3P81aF7ruE3KjXgV6oPkTX4LkUlZf+co1rGs59TJ4UDJQM73+9EiYD3MJ/G0KeyMnAak73HkRHWdypOGLrINLPyUuuUAxQ8k4UJWskEMXjMgKIlGdCcdbCWqso/lZqvQ+/bFfv5M+yhQK57JMlXfXsM3YMxUGY7IhYRhxHL3nCpxXacut2FBU6H1GCU8MuR2bJ/aNNdl8elNzCEZcRq8+s94yrI095HQvVW/tLWaNJ4ojZSsGhmzvC/xAB+VbCp7ZvI0vHPGEOQgR/bIkDa9uhuwCWtfM81VCPpBrXgwupgznxtsjFcmBSUOpTFLTW5dKoPDWOe5K4wHKd9IKbWwk+mAY+aczV4T0uMX/eKhEOWJS03cjIPN4qwdCy6zSnBWpJBoLR7QyYZKNL1MiyQ0toUsJO0ln6vaPTXOo0K7EJIxKVOYf3xR8kNsgWWe0Z4orSt56O8+v5OIYbDcSeY1hwklcMvQdGHlLyMx01D2v346mL532MHA24nRmXfDUz0ixx5rn7g4Tykvy2LBLYfrfhV4G6kvimTk5Y6I7Rif4y68+r+NHmBlZwUju0AsK0a+s5/XAg5Nc= - provider: pypi - user: hydrosquall +# provider: pypi +# user: hydrosquall install: + - tools/install_pandas.sh - python setup.py develop - pip install -U pytest-cov codecov vcrpy # - pip install -U tox-travis pytest @@ -22,6 +23,10 @@ python: - 2.7 # - 2.6 +env: + - WITH_PANDAS=false + - WITH_PANDAS=true + script: - export TIINGO_API_KEY=0000000000000000000000000000000000000000 - py.test --cov=./tiingo diff --git a/tests/test_tiingo_pandas.py b/tests/test_tiingo_pandas.py index 76d415f..c9a5f97 100644 --- a/tests/test_tiingo_pandas.py +++ b/tests/test_tiingo_pandas.py @@ -4,7 +4,7 @@ import vcr from unittest import TestCase from tiingo import TiingoClient -from tiingo.api import APIColumnNameError +from tiingo.api import APIColumnNameError, InstallPandasException try: import pandas as pd pandas_is_installed = True @@ -55,3 +55,17 @@ class TestTiingoWithPython(TestCase): prices = self._client.get_dataframe("GOOGL") assert len(prices) == 1 assert len(prices.index) == 1 + + +class TestTiingoWithoutPython(TestCase): + + def setUp(self): + if pandas_is_installed: + self.skipTest("test_tiingo_without_pandas: Pandas not installed.") + else: + self._client = TiingoClient() + + @vcr.use_cassette('tests/fixtures/ticker_price_pandas_single.yaml') + def test_get_dataframe_without_pandas(self): + with self.assertRaises(InstallPandasException): + self._client.get_dataframe("GOOGL") diff --git a/tools/install_pandas.sh b/tools/install_pandas.sh new file mode 100755 index 0000000..98ca923 --- /dev/null +++ b/tools/install_pandas.sh @@ -0,0 +1,6 @@ +#!/bin/bash +if $WITH_PANDAS +then + pip install pandas + echo "pandas installed" +fi From a4147d8793df17ec4b5e25d0b510bd8bc6d8fc6d Mon Sep 17 00:00:00 2001 From: Davis Thames Date: Fri, 11 May 2018 12:04:07 -0500 Subject: [PATCH 4/9] Removed comments --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 03f5f4b..b7114d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,8 @@ deploy: distributions: sdist bdist_wheel password: secure: VH1rgFV5XD0k3cMZxJwetBnETrFolBaM0U4K9CKQts38LF7f4xA8muY2FFFyeqR7VF2MQhYgGMcaAQQx/gRY+7AdSnqMYvxZ7p3P81aF7ruE3KjXgV6oPkTX4LkUlZf+co1rGs59TJ4UDJQM73+9EiYD3MJ/G0KeyMnAak73HkRHWdypOGLrINLPyUuuUAxQ8k4UJWskEMXjMgKIlGdCcdbCWqso/lZqvQ+/bFfv5M+yhQK57JMlXfXsM3YMxUGY7IhYRhxHL3nCpxXacut2FBU6H1GCU8MuR2bJ/aNNdl8elNzCEZcRq8+s94yrI095HQvVW/tLWaNJ4ojZSsGhmzvC/xAB+VbCp7ZvI0vHPGEOQgR/bIkDa9uhuwCWtfM81VCPpBrXgwupgznxtsjFcmBSUOpTFLTW5dKoPDWOe5K4wHKd9IKbWwk+mAY+aczV4T0uMX/eKhEOWJS03cjIPN4qwdCy6zSnBWpJBoLR7QyYZKNL1MiyQ0toUsJO0ln6vaPTXOo0K7EJIxKVOYf3xR8kNsgWWe0Z4orSt56O8+v5OIYbDcSeY1hwklcMvQdGHlLyMx01D2v346mL532MHA24nRmXfDUz0ixx5rn7g4Tykvy2LBLYfrfhV4G6kvimTk5Y6I7Rif4y68+r+NHmBlZwUju0AsK0a+s5/XAg5Nc= -# provider: pypi -# user: hydrosquall + provider: pypi + user: hydrosquall install: - tools/install_pandas.sh From deb5285a74b7e1bed66d30fe8451298d3d2978c0 Mon Sep 17 00:00:00 2001 From: Davis Thames Date: Sat, 9 Jun 2018 16:26:48 -0500 Subject: [PATCH 5/9] corrected error --- .travis.yml | 2 +- tests/test_tiingo_pandas.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index b7114d8..2e30c13 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,9 +10,9 @@ deploy: user: hydrosquall install: - - tools/install_pandas.sh - python setup.py develop - pip install -U pytest-cov codecov vcrpy + - tools/install_pandas.sh # - pip install -U tox-travis pytest language: python diff --git a/tests/test_tiingo_pandas.py b/tests/test_tiingo_pandas.py index c9a5f97..ee451eb 100644 --- a/tests/test_tiingo_pandas.py +++ b/tests/test_tiingo_pandas.py @@ -61,7 +61,7 @@ class TestTiingoWithoutPython(TestCase): def setUp(self): if pandas_is_installed: - self.skipTest("test_tiingo_without_pandas: Pandas not installed.") + self.skipTest("test_tiingo_without_pandas: Pandas is installed.") else: self._client = TiingoClient() From f7b3da50a751f854a290a0b80d3683141e31bfaf Mon Sep 17 00:00:00 2001 From: Davis Thames Date: Sun, 10 Jun 2018 14:12:26 -0500 Subject: [PATCH 6/9] addressed comments --- ...icker_price_pandas_daily_metric_name.yaml} | 14 ++++--- .../fixtures/ticker_price_pandas_single.yaml | 18 +++++---- .../fixtures/ticker_price_pandas_weekly.yaml | 14 ++++--- ..._price_pandas_weekly_multiple_tickers.yaml | 32 ++++++++------- tests/test_tiingo_pandas.py | 16 ++++++-- tiingo/api.py | 39 +++++++++++++------ tools/api_key_tool.py | 9 ----- 7 files changed, 85 insertions(+), 57 deletions(-) rename tests/fixtures/{ticker_price_pandas_daily.yaml => ticker_price_pandas_daily_metric_name.yaml} (86%) diff --git a/tests/fixtures/ticker_price_pandas_daily.yaml b/tests/fixtures/ticker_price_pandas_daily_metric_name.yaml similarity index 86% rename from tests/fixtures/ticker_price_pandas_daily.yaml rename to tests/fixtures/ticker_price_pandas_daily_metric_name.yaml index 510c79b..e714724 100644 --- a/tests/fixtures/ticker_price_pandas_daily.yaml +++ b/tests/fixtures/ticker_price_pandas_daily_metric_name.yaml @@ -2,19 +2,21 @@ interactions: - request: body: null headers: - Connection: [close] - Host: [api.tiingo.com] - User-Agent: [Python-urllib/3.6] + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Authorization: [Token 0000000000000000000000000000000000000000] + Connection: [keep-alive] + Content-Type: [application/json] + User-Agent: [tiingo-python-client 0.6.0] method: GET - uri: https://api.tiingo.com/tiingo/daily/GOOGL/prices?&startDate=2018-01-05&endDate=2018-01-19&format=json&resampleFreq=daily&token=0000000000000000000000000000000000000000 + uri: https://api.tiingo.com/tiingo/daily/GOOGL/prices?format=json&resampleFreq=daily&startDate=2018-01-05&endDate=2018-01-19 response: body: {string: '[{"date":"2018-01-05T00:00:00.000Z","close":1110.29,"high":1113.58,"low":1101.8,"open":1103.45,"volume":1493389,"adjClose":1110.29,"adjHigh":1113.58,"adjLow":1101.8,"adjOpen":1103.45,"adjVolume":1493389,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-08T00:00:00.000Z","close":1114.21,"high":1119.16,"low":1110.0,"open":1111.0,"volume":1148958,"adjClose":1114.21,"adjHigh":1119.16,"adjLow":1110.0,"adjOpen":1111.0,"adjVolume":1148958,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-09T00:00:00.000Z","close":1112.79,"high":1118.44,"low":1108.2,"open":1118.44,"volume":1335995,"adjClose":1112.79,"adjHigh":1118.44,"adjLow":1108.2,"adjOpen":1118.44,"adjVolume":1335995,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-10T00:00:00.000Z","close":1110.14,"high":1112.78,"low":1103.98,"open":1107.0,"volume":1027781,"adjClose":1110.14,"adjHigh":1112.78,"adjLow":1103.98,"adjOpen":1107.0,"adjVolume":1027781,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-11T00:00:00.000Z","close":1112.05,"high":1114.85,"low":1106.48,"open":1112.31,"volume":1102461,"adjClose":1112.05,"adjHigh":1114.85,"adjLow":1106.48,"adjOpen":1112.31,"adjVolume":1102461,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-12T00:00:00.000Z","close":1130.65,"high":1131.3,"low":1108.01,"open":1110.1,"volume":1914460,"adjClose":1130.65,"adjHigh":1131.3,"adjLow":1108.01,"adjOpen":1110.1,"adjVolume":1914460,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-16T00:00:00.000Z","close":1130.7,"high":1148.88,"low":1126.66,"open":1140.31,"volume":1783881,"adjClose":1130.7,"adjHigh":1148.88,"adjLow":1126.66,"adjOpen":1140.31,"adjVolume":1783881,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-17T00:00:00.000Z","close":1139.1,"high":1139.32,"low":1123.49,"open":1136.36,"volume":1353097,"adjClose":1139.1,"adjHigh":1139.32,"adjLow":1123.49,"adjOpen":1136.36,"adjVolume":1353097,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-18T00:00:00.000Z","close":1135.97,"high":1140.59,"low":1124.46,"open":1139.35,"volume":1333633,"adjClose":1135.97,"adjHigh":1140.59,"adjLow":1124.46,"adjOpen":1139.35,"adjVolume":1333633,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-19T00:00:00.000Z","close":1143.5,"high":1143.78,"low":1132.5,"open":1138.03,"volume":1418376,"adjClose":1143.5,"adjHigh":1143.78,"adjLow":1132.5,"adjOpen":1138.03,"adjVolume":1418376,"divCash":0.0,"splitFactor":1.0}]'} headers: Allow: ['GET, HEAD, OPTIONS'] - Connection: [close] Content-Length: ['2349'] Content-Type: [application/json] - Date: ['Thu, 10 May 2018 03:25:28 GMT'] + Date: ['Sun, 10 Jun 2018 18:24:41 GMT'] Server: [nginx/1.10.1] Vary: ['Accept, Cookie'] X-Frame-Options: [SAMEORIGIN] diff --git a/tests/fixtures/ticker_price_pandas_single.yaml b/tests/fixtures/ticker_price_pandas_single.yaml index 636eb1f..9320539 100644 --- a/tests/fixtures/ticker_price_pandas_single.yaml +++ b/tests/fixtures/ticker_price_pandas_single.yaml @@ -2,19 +2,21 @@ interactions: - request: body: null headers: - Connection: [close] - Host: [api.tiingo.com] - User-Agent: [Python-urllib/3.6] + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Authorization: [Token 0000000000000000000000000000000000000000] + Connection: [keep-alive] + Content-Type: [application/json] + User-Agent: [tiingo-python-client 0.6.0] method: GET - uri: https://api.tiingo.com/tiingo/daily/GOOGL/prices?&format=json&resampleFreq=daily&token=0000000000000000000000000000000000000000 + uri: https://api.tiingo.com/tiingo/daily/GOOGL/prices?format=json&resampleFreq=daily response: - body: {string: '[{"adjClose":1088.95,"adjHigh":1094.0,"adjLow":1062.11,"adjOpen":1064.1,"adjVolume":2357979,"close":1088.95,"date":"2018-05-09T00:00:00+00:00","divCash":0.0,"high":1094.0,"low":1062.11,"open":1064.1,"splitFactor":1.0,"volume":2357979}]'} + body: {string: '[{"adjClose":1132.71,"adjHigh":1138.78,"adjLow":1123.23,"adjOpen":1131.21,"adjVolume":1364226,"close":1132.71,"date":"2018-06-08T00:00:00+00:00","divCash":0.0,"high":1138.78,"low":1123.23,"open":1131.21,"splitFactor":1.0,"volume":1364226}]'} headers: Allow: ['GET, HEAD, OPTIONS'] - Connection: [close] - Content-Length: ['235'] + Content-Length: ['239'] Content-Type: [application/json] - Date: ['Thu, 10 May 2018 03:25:28 GMT'] + Date: ['Sun, 10 Jun 2018 18:21:17 GMT'] Server: [nginx/1.10.1] Vary: ['Accept, Cookie'] X-Frame-Options: [SAMEORIGIN] diff --git a/tests/fixtures/ticker_price_pandas_weekly.yaml b/tests/fixtures/ticker_price_pandas_weekly.yaml index 0c49b0c..59886a1 100644 --- a/tests/fixtures/ticker_price_pandas_weekly.yaml +++ b/tests/fixtures/ticker_price_pandas_weekly.yaml @@ -2,19 +2,21 @@ interactions: - request: body: null headers: - Connection: [close] - Host: [api.tiingo.com] - User-Agent: [Python-urllib/3.6] + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Authorization: [Token 0000000000000000000000000000000000000000] + Connection: [keep-alive] + Content-Type: [application/json] + User-Agent: [tiingo-python-client 0.6.0] method: GET - uri: https://api.tiingo.com/tiingo/daily/GOOGL/prices?&startDate=2018-01-05&endDate=2018-01-19&format=json&resampleFreq=weekly&token=0000000000000000000000000000000000000000 + uri: https://api.tiingo.com/tiingo/daily/GOOGL/prices?format=json&resampleFreq=weekly&startDate=2018-01-05&endDate=2018-01-19 response: body: {string: '[{"date":"2018-01-05T00:00:00.000Z","close":1110.29,"high":1113.58,"low":1053.02,"open":1053.02,"volume":5889084,"adjClose":1110.29,"adjHigh":1113.58,"adjLow":1053.02,"adjOpen":1053.02,"adjVolume":5889084,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-12T00:00:00.000Z","close":1130.65,"high":1131.3,"low":1103.98,"open":1111.0,"volume":6529655,"adjClose":1130.65,"adjHigh":1131.3,"adjLow":1103.98,"adjOpen":1111.0,"adjVolume":6529655,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-19T00:00:00.000Z","close":1135.97,"high":1148.88,"low":1123.49,"open":1140.31,"volume":4470611,"adjClose":1135.97,"adjHigh":1148.88,"adjLow":1123.49,"adjOpen":1140.31,"adjVolume":4470611,"divCash":0.0,"splitFactor":1.0}]'} headers: Allow: ['GET, HEAD, OPTIONS'] - Connection: [close] Content-Length: ['708'] Content-Type: [application/json] - Date: ['Thu, 10 May 2018 03:25:29 GMT'] + Date: ['Sun, 10 Jun 2018 18:25:46 GMT'] Server: [nginx/1.10.1] Vary: ['Accept, Cookie'] X-Frame-Options: [SAMEORIGIN] diff --git a/tests/fixtures/ticker_price_pandas_weekly_multiple_tickers.yaml b/tests/fixtures/ticker_price_pandas_weekly_multiple_tickers.yaml index c531d47..3247319 100644 --- a/tests/fixtures/ticker_price_pandas_weekly_multiple_tickers.yaml +++ b/tests/fixtures/ticker_price_pandas_weekly_multiple_tickers.yaml @@ -2,19 +2,21 @@ interactions: - request: body: null headers: - Connection: [close] - Host: [api.tiingo.com] - User-Agent: [Python-urllib/3.6] + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Authorization: [Token 0000000000000000000000000000000000000000] + Connection: [keep-alive] + Content-Type: [application/json] + User-Agent: [tiingo-python-client 0.6.0] method: GET - uri: https://api.tiingo.com/tiingo/daily/GOOGL/prices?&startDate=2018-01-05&endDate=2018-01-19&format=json&resampleFreq=weekly&token=0000000000000000000000000000000000000000 + uri: https://api.tiingo.com/tiingo/daily/GOOGL/prices?format=json&resampleFreq=weekly&startDate=2018-01-05&endDate=2018-01-19 response: body: {string: '[{"date":"2018-01-05T00:00:00.000Z","close":1110.29,"high":1113.58,"low":1053.02,"open":1053.02,"volume":5889084,"adjClose":1110.29,"adjHigh":1113.58,"adjLow":1053.02,"adjOpen":1053.02,"adjVolume":5889084,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-12T00:00:00.000Z","close":1130.65,"high":1131.3,"low":1103.98,"open":1111.0,"volume":6529655,"adjClose":1130.65,"adjHigh":1131.3,"adjLow":1103.98,"adjOpen":1111.0,"adjVolume":6529655,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-19T00:00:00.000Z","close":1135.97,"high":1148.88,"low":1123.49,"open":1140.31,"volume":4470611,"adjClose":1135.97,"adjHigh":1148.88,"adjLow":1123.49,"adjOpen":1140.31,"adjVolume":4470611,"divCash":0.0,"splitFactor":1.0}]'} headers: Allow: ['GET, HEAD, OPTIONS'] - Connection: [close] Content-Length: ['708'] Content-Type: [application/json] - Date: ['Thu, 10 May 2018 03:25:29 GMT'] + Date: ['Sun, 10 Jun 2018 18:33:00 GMT'] Server: [nginx/1.10.1] Vary: ['Accept, Cookie'] X-Frame-Options: [SAMEORIGIN] @@ -22,19 +24,21 @@ interactions: - request: body: null headers: - Connection: [close] - Host: [api.tiingo.com] - User-Agent: [Python-urllib/3.6] + Accept: ['*/*'] + Accept-Encoding: ['gzip, deflate'] + Authorization: [Token 0000000000000000000000000000000000000000] + Connection: [keep-alive] + Content-Type: [application/json] + User-Agent: [tiingo-python-client 0.6.0] method: GET - uri: https://api.tiingo.com/tiingo/daily/AAPL/prices?&startDate=2018-01-05&endDate=2018-01-19&format=json&resampleFreq=weekly&token=0000000000000000000000000000000000000000 + uri: https://api.tiingo.com/tiingo/daily/AAPL/prices?format=json&resampleFreq=weekly&startDate=2018-01-05&endDate=2018-01-19 response: - body: {string: '[{"date":"2018-01-05T00:00:00.000Z","close":175.0,"high":175.37,"low":169.26,"open":170.16,"volume":99095223,"adjClose":174.297949567,"adjHigh":174.6664652318,"adjLow":168.5809768212,"adjOpen":169.4773662761,"adjVolume":99095223,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-12T00:00:00.000Z","close":177.09,"high":177.36,"low":173.0,"open":174.35,"volume":107548622,"adjClose":176.379565079,"adjHigh":176.6484819154,"adjLow":172.3059730005,"adjOpen":173.6505571829,"adjVolume":107548622,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-19T00:00:00.000Z","close":179.26,"high":180.1,"low":175.07,"open":177.9,"volume":92146251,"adjClose":178.5408596536,"adjHigh":179.3774898115,"adjLow":174.3676687468,"adjOpen":177.1863155884,"adjVolume":92146251,"divCash":0.0,"splitFactor":1.0}]'} + body: {string: '[{"date":"2018-01-05T00:00:00.000Z","close":175.0,"high":175.37,"low":169.26,"open":170.16,"volume":99095223,"adjClose":173.6258731716,"adjHigh":173.9929678748,"adjLow":167.9309445315,"adjOpen":168.8238775936,"adjVolume":99095223,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-12T00:00:00.000Z","close":177.09,"high":177.36,"low":173.0,"open":174.35,"volume":107548622,"adjClose":175.6994621711,"adjHigh":175.9673420898,"adjLow":171.6415774782,"adjOpen":172.9809770712,"adjVolume":107548622,"divCash":0.0,"splitFactor":1.0},{"date":"2018-01-19T00:00:00.000Z","close":179.26,"high":180.1,"low":175.07,"open":177.9,"volume":92146251,"adjClose":177.8524229985,"adjHigh":178.6858271897,"adjLow":173.6953235208,"adjOpen":176.503101927,"adjVolume":92146251,"divCash":0.0,"splitFactor":1.0}]'} headers: Allow: ['GET, HEAD, OPTIONS'] - Connection: [close] - Content-Length: ['786'] + Content-Length: ['787'] Content-Type: [application/json] - Date: ['Thu, 10 May 2018 03:25:30 GMT'] + Date: ['Sun, 10 Jun 2018 18:33:01 GMT'] Server: [nginx/1.10.1] Vary: ['Accept, Cookie'] X-Frame-Options: [SAMEORIGIN] diff --git a/tests/test_tiingo_pandas.py b/tests/test_tiingo_pandas.py index ee451eb..f84e58d 100644 --- a/tests/test_tiingo_pandas.py +++ b/tests/test_tiingo_pandas.py @@ -26,6 +26,7 @@ class TestTiingoWithPython(TestCase): prices = self._client.get_dataframe("GOOGL", startDate='2018-01-05', endDate='2018-01-19', frequency='weekly') self.assertTrue(isinstance(prices, pd.DataFrame)) + assert len(prices.index) == 3 @vcr.use_cassette('tests/fixtures/ticker_price_pandas_weekly_multiple_tickers.yaml') def test_return_pandas_format_multiple(self): @@ -34,8 +35,8 @@ class TestTiingoWithPython(TestCase): prices = self._client.get_dataframe(tickers, startDate='2018-01-05', endDate='2018-01-19', metric_name='adjClose', frequency='weekly') self.assertTrue(isinstance(prices, pd.DataFrame)) - assert prices['GOOGL'].loc['2018-01-05'] == 1110.29 - self.assertAlmostEqual(prices['AAPL'].loc['2018-01-19'], 178.54, 2) + assert len(prices.columns) == 2 + assert len(prices.index) == 3 @vcr.use_cassette('tests/fixtures/ticker_price_pandas_daily.yaml') def test_return_pandas_daily(self): @@ -43,7 +44,16 @@ class TestTiingoWithPython(TestCase): prices = self._client.get_dataframe("GOOGL", startDate='2018-01-05', endDate='2018-01-19', frequency='daily') self.assertTrue(isinstance(prices, pd.DataFrame)) - assert prices['adjClose'].loc['2018-01-05'] == 1110.29 + assert len(prices.columns) == 12 + + @vcr.use_cassette('tests/fixtures/ticker_price_pandas_daily_metric_name.yaml') + def test_return_pandas_daily(self): + """Test that one column is returned when a metric name is specified""" + + prices = self._client.get_dataframe("GOOGL", startDate='2018-01-05', metric_name='adjClose', + endDate='2018-01-19', frequency='daily') + self.assertTrue(isinstance(prices, pd.Series)) + assert len(prices.index) == 10 def test_column_error(self): with self.assertRaises(APIColumnNameError): diff --git a/tiingo/api.py b/tiingo/api.py index d3698ad..ea865a5 100644 --- a/tiingo/api.py +++ b/tiingo/api.py @@ -177,7 +177,7 @@ class TiingoClient(RestClient): return url def get_dataframe(self, tickers, - startDate=None, endDate=None, metric_name='adjClose', frequency='daily'): + startDate=None, endDate=None, metric_name=None, frequency='daily'): """ Return a pandas.DataFrame of historical prices for one or more ticker symbols. @@ -200,24 +200,41 @@ class TiingoClient(RestClient): 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)) + + params = { + 'format': 'json', + 'resampleFreq': frequency + } + if startDate: + params['startDate'] = startDate + if endDate: + params['endDate'] = endDate + if pandas_is_installed: - prices = pd.DataFrame() if type(tickers) is str: stock = tickers - url = self._build_url(stock, startDate, endDate, frequency) - prices = pd.read_json(url) - prices.index = prices['date'] - del(prices['date']) + url = "tiingo/daily/{}/prices".format(stock) + response = self._request('GET', url, params=params) + df = pd.DataFrame(response.json()) + if metric_name is not None: + prices = df[metric_name] + else: + prices = df + prices.index = df['date'] + del (prices['date']) else: - if metric_name not in valid_columns: - raise APIColumnNameError('Valid data items are: '+str(valid_columns)) + prices = pd.DataFrame() for stock in tickers: - url = self._build_url(stock, startDate, endDate, frequency) - df = pd.read_json(url) + url = "tiingo/daily/{}/prices".format(stock) + response = self._request('GET', url, params=params) + df = pd.DataFrame(response.json()) df.index = df['date'] df.rename(index=str, columns={metric_name: stock}, inplace=True) prices = pd.concat([prices, df[stock]], axis=1) - prices.index = pd.to_datetime(prices.index) + prices.index = pd.to_datetime(prices.index) return prices else: error_message = ("Pandas is not installed, but .get_ticker_price() was " diff --git a/tools/api_key_tool.py b/tools/api_key_tool.py index dd72f10..04bea0e 100755 --- a/tools/api_key_tool.py +++ b/tools/api_key_tool.py @@ -12,11 +12,6 @@ zero_api_regex = r'(\[Token )0{40}(\])' real_api_regex = r'(\[Token ).{40}(\])' zero_token_string = '[Token ' + 40 * '0' + ']' -# pandas json api call configuration -pd_real_api_regex = r'&token=.{40}' -pd_zero_api_regex = r'&token=0{40}' -pd_zero_token_string = '&token=' + 40 * '0' - def has_api_key(file_name): """ @@ -30,9 +25,6 @@ def has_api_key(file_name): if re.search(real_api_regex, text) is not None and \ re.search(zero_api_regex, text) is None: return True - elif re.search(pd_real_api_regex, text) is not None and \ - re.search(pd_zero_api_regex, text) is None: - return True return False @@ -44,7 +36,6 @@ def remove_api_key(file_name): with open(file_name, 'r') as fp: text = fp.read() text = re.sub(real_api_regex, zero_token_string, text) - text = re.sub(pd_real_api_regex, pd_zero_token_string, text) with open(file_name, 'w') as fp: fp.write(text) return From a2b4d34ab141ff1cbdb416ce720d843134dd3208 Mon Sep 17 00:00:00 2001 From: Davis Thames Date: Sun, 10 Jun 2018 14:13:28 -0500 Subject: [PATCH 7/9] reverted to prior indexing method --- tests/test_tiingo_pandas.py | 3 ++- tiingo/api.py | 12 ++---------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/tests/test_tiingo_pandas.py b/tests/test_tiingo_pandas.py index f84e58d..f36123d 100644 --- a/tests/test_tiingo_pandas.py +++ b/tests/test_tiingo_pandas.py @@ -55,10 +55,11 @@ class TestTiingoWithPython(TestCase): self.assertTrue(isinstance(prices, pd.Series)) assert len(prices.index) == 10 - def test_column_error(self): + def test_metric_name_column_error(self): with self.assertRaises(APIColumnNameError): self._client.get_dataframe(['GOOGL', 'AAPL'], startDate='2018-01-05', endDate='2018-01-19', metric_name='xopen', frequency='weekly') + @vcr.use_cassette('tests/fixtures/ticker_price_pandas_single.yaml') def test_pandas_edge_case(self): """Test single price/date being returned as a frame""" diff --git a/tiingo/api.py b/tiingo/api.py index ea865a5..427d217 100644 --- a/tiingo/api.py +++ b/tiingo/api.py @@ -166,16 +166,6 @@ class TiingoClient(RestClient): else: return response.content.decode("utf-8") - def _build_url(self, stock, startDate, endDate, frequency): - url = "https://api.tiingo.com/tiingo/" - url += "daily/{}/prices?".format(stock) - if startDate is not None and endDate is None: - url += "&startDate={}".format(startDate) - if startDate is not None and endDate is not None: - url += "&startDate={}&endDate={}".format(startDate, endDate) - url += "&format=json&resampleFreq={}&token={}".format(frequency, self._api_key) - return url - def get_dataframe(self, tickers, startDate=None, endDate=None, metric_name=None, frequency='daily'): @@ -186,6 +176,7 @@ class TiingoClient(RestClient): 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. @@ -221,6 +212,7 @@ class TiingoClient(RestClient): df = pd.DataFrame(response.json()) if metric_name is not None: prices = df[metric_name] + prices.index = df['date'] else: prices = df prices.index = df['date'] From 5de3306eeaca1f3462e82d94767e6452bda13bf4 Mon Sep 17 00:00:00 2001 From: Davis Thames Date: Mon, 11 Jun 2018 11:59:13 -0500 Subject: [PATCH 8/9] updated for pandas method --- README.rst | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 4cd8a4b..b568cd4 100644 --- a/README.rst +++ b/README.rst @@ -25,7 +25,7 @@ Tiingo Python Tiingo is a financial data platform that makes high quality financial tools available to all. Tiingo has a REST and Real-Time Data API, which this library helps you to access. Presently, the API includes support for the following endpoints: * Stock Market Ticker Closing Prices + Metadata. Data includes full distribution details and is validated using a proprietary EOD Price Engine. -* Curated news from top financial news sources + blogs. Stories are tagged with topic tags and relevant stock tickers by Tiingo's algorithms. +* Curated news from top financial news sources + blogs. Stories are tagged with topic tags and relevant stock tickers by Tiingo's algorithms. Usage @@ -37,6 +37,12 @@ First, install the library from PyPi: pip install tiingo +If you prefer to receive your results in ``pandas DataFrame`` or ``Series`` format, and you do not already have pandas installed, install it as an optional dependency: + +.. code-block:: shell + + pip install tiingo[pandas] + Next, initialize your client. It is recommended to use an environment variable to initialize your client for convenience. @@ -67,7 +73,7 @@ Alternately, you may use a dictionary to customize/authorize your client. 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 - + # Get Ticker ticker_metadata = client.get_ticker_metadata("GOOGL") @@ -86,13 +92,35 @@ Now you can use ``TiingoClient`` to make your API calls. (Other parameters are a tickers = client.list_stock_tickers() # Get news articles about given tickers or search terms from given domains - articles = client.get_news(tickers=['GOOGL', 'APPL'], - tags=['Laptops'], + articles = client.get_news(tickers=['GOOGL', 'AAPL'], + tags=['Laptops'], sources=['washingtonpost.com'], startDate='2017-01-01', endDate='2017-08-31') +To receive results in ``pandas`` format, use the ``get_dataframe()`` method: + +.. code-block:: python + + #Get a pd.DataFrame of the price history of a single symbol (default is daily): + ticker_history = client.get_dataframe("GOOGL") + + #The method returns all of the available information on a symbol, such as open, high, low, close, adjusted close, etc. This page in the tiingo api documentation lists the available information on each symbol: https://api.tiingo.com/docs/tiingo/daily#priceData. + + #Frequencies and start and end dates can be specified similarly to the json method above. + + #Get a pd.Series of only one column of the available response data by specifying one of the valid the 'metric_name' parameters: + ticker_history = client.get_dataframe("GOOGL", metric_name='adjClose') + + #Get a pd.DataFrame for a list of symbols for a specified metric_name (default is adjClose if no metric_name is specified): + ticker_history = client.get_dataframe(['GOOGL', 'AAPL'], + frequency='weekly', + metric_name='volume', + startDate='2017-01-01', + endDate='2018-05-31') + + Further Docs -------- @@ -110,7 +138,7 @@ Roadmap: -------- * Client-side validation of tickers -* Data validation of returned responses +* Data validation of returned responses * Case insensitivity for ticker names * More documentation / code examples From faa93e4146d4d213cca924f3761b6f2d6e25e3f2 Mon Sep 17 00:00:00 2001 From: Davis Thames Date: Mon, 11 Jun 2018 12:04:19 -0500 Subject: [PATCH 9/9] fixed wordwraps --- README.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index b568cd4..68e9d1d 100644 --- a/README.rst +++ b/README.rst @@ -106,14 +106,18 @@ To receive results in ``pandas`` format, use the ``get_dataframe()`` method: #Get a pd.DataFrame of the price history of a single symbol (default is daily): ticker_history = client.get_dataframe("GOOGL") - #The method returns all of the available information on a symbol, such as open, high, low, close, adjusted close, etc. This page in the tiingo api documentation lists the available information on each symbol: https://api.tiingo.com/docs/tiingo/daily#priceData. + #The method returns all of the available information on a symbol, such as open, high, low, close, + #adjusted close, etc. This page in the tiingo api documentation lists the available information on each + #symbol: https://api.tiingo.com/docs/tiingo/daily#priceData. #Frequencies and start and end dates can be specified similarly to the json method above. - #Get a pd.Series of only one column of the available response data by specifying one of the valid the 'metric_name' parameters: + #Get a pd.Series of only one column of the available response data by specifying one of the valid the + #'metric_name' parameters: ticker_history = client.get_dataframe("GOOGL", metric_name='adjClose') - #Get a pd.DataFrame for a list of symbols for a specified metric_name (default is adjClose if no metric_name is specified): + #Get a pd.DataFrame for a list of symbols for a specified metric_name (default is adjClose if no + #metric_name is specified): ticker_history = client.get_dataframe(['GOOGL', 'AAPL'], frequency='weekly', metric_name='volume',