issue#85 initial commit

This commit is contained in:
Davis Thames
2018-05-09 22:26:18 -05:00
parent 1869f34c8a
commit e3f26f609b
4 changed files with 148 additions and 7 deletions

View File

@@ -57,6 +57,7 @@ setup(
packages=find_packages(include=[NAME]), packages=find_packages(include=[NAME]),
include_package_data=True, include_package_data=True,
install_requires=requirements, install_requires=requirements,
extras_require={'pandas': ['pandas>=0.18']},
license="MIT license", license="MIT license",
zip_safe=False, zip_safe=False,
keywords=['tiingo', 'finance', 'stocks'], keywords=['tiingo', 'finance', 'stocks'],

View File

@@ -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

View File

@@ -7,10 +7,13 @@ import csv
import json import json
from collections import namedtuple from collections import namedtuple
from zipfile import ZipFile from zipfile import ZipFile
from tiingo.restclient import RestClient from tiingo.restclient import RestClient
import requests import requests
try:
import pandas as pd
pandas_is_installed = True
except ImportError:
pandas_is_installed = False
VERSION = pkg_resources.get_distribution("tiingo").version VERSION = pkg_resources.get_distribution("tiingo").version
@@ -44,6 +47,13 @@ def dict_to_object(item, object_name):
object_hook=lambda d: object_hook=lambda d:
namedtuple(object_name, fields)(*values)) namedtuple(object_name, fields)(*values))
class InstallPandasException(Exception):
pass
class APIColumnNameError(Exception):
pass
class TiingoClient(RestClient): class TiingoClient(RestClient):
"""Class for managing interactions with the Tiingo REST API """Class for managing interactions with the Tiingo REST API
@@ -60,6 +70,7 @@ class TiingoClient(RestClient):
api_key = self._config['api_key'] api_key = self._config['api_key']
except KeyError: except KeyError:
api_key = os.environ.get('TIINGO_API_KEY') api_key = os.environ.get('TIINGO_API_KEY')
self._api_key = api_key
if not(api_key): if not(api_key):
raise RuntimeError("Tiingo API Key not provided. Please provide" raise RuntimeError("Tiingo API Key not provided. Please provide"
@@ -155,6 +166,67 @@ class TiingoClient(RestClient):
else: else:
return response.content.decode("utf-8") 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 # NEWS FEEDS
# tiingo/news # tiingo/news
def get_news(self, tickers=[], tags=[], sources=[], startDate=None, def get_news(self, tickers=[], tags=[], sources=[], startDate=None,

View File

@@ -6,35 +6,46 @@ import re
import argparse import argparse
fixtures_directory = 'tests/fixtures/' fixtures_directory = 'tests/fixtures/'
# restclient api header configuration
zero_api_regex = r'(\[Token )0{40}(\])' zero_api_regex = r'(\[Token )0{40}(\])'
real_api_regex = r'(\[Token ).{40}(\])' real_api_regex = r'(\[Token ).{40}(\])'
zero_token_string = '[Token ' + 40 * '0' + ']' 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'. Detect whether the file contains an api key in the Token object that is not 40*'0'.
See issue #86. See issue #86.
:param file: path-to-file to check :param file: path-to-file to check
:return: boolean :return: boolean
""" """
f = open(file, 'r') f = open(file_name, 'r')
text = f.read() text = f.read()
if re.search(real_api_regex, text) is not None and \ if re.search(real_api_regex, text) is not None and \
re.search(zero_api_regex, text) is None: re.search(zero_api_regex, text) is None:
return True 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 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. Change the api key in the Token object to 40*'0'. See issue #86.
:param file: path-to-file to change :param file: path-to-file to change
""" """
with open(file, 'r') as fp: with open(file_name, 'r') as fp:
text = fp.read() text = fp.read()
text = re.sub(real_api_regex, zero_token_string, text) 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) fp.write(text)
return return