mirror of
https://github.com/hydrosquall/tiingo-python.git
synced 2025-12-17 11:54:19 +01:00
issue#85 initial commit
This commit is contained in:
1
setup.py
1
setup.py
@@ -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'],
|
||||||
|
|||||||
57
tests/test_tiingo_pandas.py
Normal file
57
tests/test_tiingo_pandas.py
Normal 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
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user