From 3d8d213012f6defffd7f82fc21a36a2de588ef36 Mon Sep 17 00:00:00 2001 From: Patrick Collins <54278053+PatrickAlphaVantage@users.noreply.github.com> Date: Sun, 26 Apr 2020 16:52:42 -0400 Subject: [PATCH] Prep for 2.2.0 (#210) * added rapidapi key integration * prep for 2.1.3 * Removing get_batch_stock_quotes method (#189) * Removing get_batch_stock_quotes method Remove get_batch_stock_quotes method, resolving issue #184. * remove tests for get_batch_stock_quotes * Delete mock_batch_quotes Was only used for the tests removed in the previous commits, which made this file dead weight. * Add asyncio support for all components (#191) * Add asyncio support for all components In order to minimize the copy-paste between the async and sync implementations, all of the non-base components are sym-linked to their sync counterparts. Since these import `AlphaVantage` locally, they automatically pick up the async version, which is really nifty! There was some refactoring in `SectorPerformance` to make the re-use possible. The async verison of AlphaVantage tries to re-use as much as possible from the sync version. The main differences are: * a new `session` for the HTTP calls * a new `close()` function which should be called when you're done * `proxy` is a string instead of a dict Using it: ```python import asyncio from alpha_vantage.async_support.timeseries import TimeSeries async def get_data(): ts = TimeSeries(key='YOUR_API_KEY') data, meta_data = await ts.get_intraday('GOOGL') await ts.close() loop = asyncio.get_event_loop() loop.run_until_complete(get_data()) loop.close() ``` * Add asyncio packages to setup.py * Issue #206: Add 'time_period' argument to 'get_bbands()' documentation. (#207) * Fixes small documentation bugs (#208) * fixed fx documentation * fixed pypi badge for documentation * prep for 2.2.0 (#209) * fixed fx documentation * fixed pypi badge for documentation * prep for 2.2.0 * small documentaiton change for 2.2.0 Co-authored-by: Aaron Sanders Co-authored-by: Jon Cinque Co-authored-by: Peter Anderson --- README.md | 38 ++- alpha_vantage/alphavantage.py | 51 ++- alpha_vantage/async_support/__init__.py | 0 alpha_vantage/async_support/alphavantage.py | 272 ++++++++++++++++ .../async_support/cryptocurrencies.py | 1 + .../async_support/foreignexchange.py | 1 + .../async_support/sectorperformance.py | 1 + alpha_vantage/async_support/techindicators.py | 1 + alpha_vantage/async_support/timeseries.py | 1 + alpha_vantage/sectorperformance.py | 51 +-- alpha_vantage/techindicators.py | 5 +- alpha_vantage/timeseries.py | 12 - docs/conf.py | 4 +- docs/index.rst | 3 +- setup.py | 8 +- test_alpha_vantage/test_alphavantage.py | 32 +- test_alpha_vantage/test_alphavantage_async.py | 235 ++++++++++++++ .../test_data/mock_batch_quotes | 27 -- .../test_integration_alphavantage.py | 2 +- .../test_integration_alphavantage_async.py | 298 ++++++++++++++++++ 20 files changed, 912 insertions(+), 131 deletions(-) create mode 100644 alpha_vantage/async_support/__init__.py create mode 100644 alpha_vantage/async_support/alphavantage.py create mode 120000 alpha_vantage/async_support/cryptocurrencies.py create mode 120000 alpha_vantage/async_support/foreignexchange.py create mode 120000 alpha_vantage/async_support/sectorperformance.py create mode 120000 alpha_vantage/async_support/techindicators.py create mode 120000 alpha_vantage/async_support/timeseries.py create mode 100644 test_alpha_vantage/test_alphavantage_async.py delete mode 100644 test_alpha_vantage/test_data/mock_batch_quotes create mode 100644 test_alpha_vantage/test_integration_alphavantage_async.py diff --git a/README.md b/README.md index c38791f..c5e5105 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Vantage (http://www.alphavantage.co/). It requires a free API key, that can be r ## News +* From version 2.2.0 onwards, asyncio support now provided. See below for more information. * From version 2.1.3 onwards, [rapidAPI](https://rapidapi.com/alphavantage/api/alpha-vantage-alpha-vantage-default) key integration is now available. * From version 2.1.0 onwards, error logging of bad API calls has been made more apparent. * From version 1.9.0 onwards, the urllib was substituted by pythons request library that is thread safe. If you have any error, post an issue. @@ -159,7 +160,7 @@ Giving us as output: ### Foreign Exchange (FX) -The foreign exchange is just metadata, thus only available as json format (using the 'csv' or 'pandas' format will raise an Error) +The foreign exchange endpoint has no metadata, thus only available as json format and pandas (using the 'csv' format will raise an Error) ```python from alpha_vantage.foreignexchange import ForeignExchange @@ -182,6 +183,36 @@ Giving us as output: } ``` +### Asyncio support + +From version 2.2.0 on, asyncio support will now be available. This is only for python versions 3.5+. If you do not have 3.5+, the code will break. + +The syntax is simple, just mark your methods with the `async` keyword, and use the `await` keyword. + +Here is an example of a for loop for getting multiple symbols asyncronously. This greatly improving the performance of a program with multiple API calls. + +```python +import asyncio +from alpha_vantage.async_support.timeseries import TimeSeries + +symbols = ['AAPL', 'GOOG', 'TSLA', 'MSFT'] + + +async def get_data(symbol): + ts = TimeSeries(key='YOUR_KEY_HERE') + data, _ = await ts.get_quote_endpoint(symbol) + await ts.close() + return data + +loop = asyncio.get_event_loop() +tasks = [get_data(symbol) for symbol in symbols] +group1 = asyncio.gather(*tasks) +results = loop.run_until_complete(group1) +loop.close() +print(results) +``` + + ## Examples I have added a repository with examples in a python notebook to better see the @@ -207,13 +238,16 @@ Contributing is always welcome. Just contact us on how best you can contribute, * The integration tests are not being run at the moment within travis, gotta fix them to run. * Add test for csv calls as well. * Add tests for incompatible parameter raise errors. +* Github actions & other items in the issues page. ## Contact: -You can reach the Alpha Vantage team on any of the following platforms: +You can reach/follow the Alpha Vantage team on any of the following platforms: * [Slack](https://alphavantage.herokuapp.com/) * [Twitter: @alpha_vantage](https://twitter.com/alpha_vantage) +* [Medium-Patrick](https://medium.com/@patrick.collins_58673) +* [Medium-AlphaVantage](https://medium.com/alpha-vantage) * Email: support@alphavantage.co diff --git a/alpha_vantage/alphavantage.py b/alpha_vantage/alphavantage.py index 00694be..be501f4 100644 --- a/alpha_vantage/alphavantage.py +++ b/alpha_vantage/alphavantage.py @@ -3,6 +3,7 @@ from functools import wraps import inspect import sys +import re # Pandas became an optional dependency, but we still want to track it try: import pandas @@ -38,9 +39,9 @@ def __init__(self, key=None, output_format='json', by the alpha vantage api call or 'integer' if you just want an integer indexing on your dataframe. Only valid, when the output_format is 'pandas' - proxy: Dictionary mapping protocol or protocol and hostname to + proxy: Dictionary mapping protocol or protocol and hostname to the URL of the proxy. - rapidapi: Boolean describing whether or not the API key is + rapidapi: Boolean describing whether or not the API key is through the RapidAPI platform or not """ if key is None: @@ -159,6 +160,50 @@ def _call_wrapper(self, *args, **kwargs): return self._handle_api_call(url), data_key, meta_data_key return _call_wrapper + @classmethod + def _output_format_sector(cls, func, override=None): + """ Decorator in charge of giving the output its right format, either + json or pandas (replacing the % for usable floats, range 0-1.0) + + Keyword Arguments: + func: The function to be decorated + override: Override the internal format of the call, default None + Returns: + A decorator for the format sector api call + """ + @wraps(func) + def _format_wrapper(self, *args, **kwargs): + json_response, data_key, meta_data_key = func( + self, *args, **kwargs) + if isinstance(data_key, list): + # Replace the strings into percentage + data = {key: {k: self.percentage_to_float(v) + for k, v in json_response[key].items()} for key in data_key} + else: + data = json_response[data_key] + # TODO: Fix orientation in a better way + meta_data = json_response[meta_data_key] + # Allow to override the output parameter in the call + if override is None: + output_format = self.output_format.lower() + elif 'json' or 'pandas' in override.lower(): + output_format = override.lower() + # Choose output format + if output_format == 'json': + return data, meta_data + elif output_format == 'pandas': + data_pandas = pandas.DataFrame.from_dict(data, + orient='columns') + # Rename columns to have a nicer name + col_names = [re.sub(r'\d+.', '', name).strip(' ') + for name in list(data_pandas)] + data_pandas.columns = col_names + return data_pandas, meta_data + else: + raise ValueError('Format: {} is not supported'.format( + self.output_format)) + return _format_wrapper + @classmethod def _output_format(cls, func, override=None): """ Decorator in charge of giving the output its right format, either @@ -237,7 +282,7 @@ def set_proxy(self, proxy=None): """ Set a new proxy configuration Keyword Arguments: - proxy: Dictionary mapping protocol or protocol and hostname to + proxy: Dictionary mapping protocol or protocol and hostname to the URL of the proxy. """ self.proxy = proxy or {} diff --git a/alpha_vantage/async_support/__init__.py b/alpha_vantage/async_support/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/alpha_vantage/async_support/alphavantage.py b/alpha_vantage/async_support/alphavantage.py new file mode 100644 index 0000000..dc317da --- /dev/null +++ b/alpha_vantage/async_support/alphavantage.py @@ -0,0 +1,272 @@ +import aiohttp +from functools import wraps +import inspect +import re +# Pandas became an optional dependency, but we still want to track it +try: + import pandas + _PANDAS_FOUND = True +except ImportError: + _PANDAS_FOUND = False +import csv +from ..alphavantage import AlphaVantage as AlphaVantageBase + + +class AlphaVantage(AlphaVantageBase): + """ + Async version of the base class where the decorators and base function for + the other classes of this python wrapper will inherit from. + """ + + def __init__(self, *args, proxy=None, **kwargs): + super(AlphaVantage, self).__init__(*args, **kwargs) + self.session = None + self.proxy = proxy or '' + + @classmethod + def _call_api_on_func(cls, func): + """ Decorator for forming the api call with the arguments of the + function, it works by taking the arguments given to the function + and building the url to call the api on it + + Keyword Arguments: + func: The function to be decorated + """ + + # Argument Handling + argspec = inspect.getfullargspec(func) + try: + # Asumme most of the cases have a mixed between args and named + # args + positional_count = len(argspec.args) - len(argspec.defaults) + defaults = dict( + zip(argspec.args[positional_count:], argspec.defaults)) + except TypeError: + if argspec.args: + # No defaults + positional_count = len(argspec.args) + defaults = {} + elif argspec.defaults: + # Only defaults + positional_count = 0 + defaults = argspec.defaults + # Actual decorating + + @wraps(func) + async def _call_wrapper(self, *args, **kwargs): + used_kwargs = kwargs.copy() + # Get the used positional arguments given to the function + used_kwargs.update(zip(argspec.args[positional_count:], + args[positional_count:])) + # Update the dictionary to include the default parameters from the + # function + used_kwargs.update({k: used_kwargs.get(k, d) + for k, d in defaults.items()}) + # Form the base url, the original function called must return + # the function name defined in the alpha vantage api and the data + # key for it and for its meta data. + function_name, data_key, meta_data_key = func( + self, *args, **kwargs) + base_url = AlphaVantage._RAPIDAPI_URL if self.rapidapi else AlphaVantage._ALPHA_VANTAGE_API_URL + url = "{}function={}".format(base_url, function_name) + for idx, arg_name in enumerate(argspec.args[1:]): + try: + arg_value = args[idx] + except IndexError: + arg_value = used_kwargs[arg_name] + if 'matype' in arg_name and arg_value: + # If the argument name has matype, we gotta map the string + # or the integer + arg_value = self.map_to_matype(arg_value) + if arg_value: + # Discard argument in the url formation if it was set to + # None (in other words, this will call the api with its + # internal defined parameter) + if isinstance(arg_value, tuple) or isinstance(arg_value, list): + # If the argument is given as list, then we have to + # format it, you gotta format it nicely + arg_value = ','.join(arg_value) + url = '{}&{}={}'.format(url, arg_name, arg_value) + # Allow the output format to be json or csv (supported by + # alphavantage api). Pandas is simply json converted. + if 'json' in self.output_format.lower() or 'csv' in self.output_format.lower(): + oformat = self.output_format.lower() + elif 'pandas' in self.output_format.lower(): + oformat = 'json' + else: + raise ValueError("Output format: {} not recognized, only json," + "pandas and csv are supported".format( + self.output_format.lower())) + apikey_parameter = "" if self.rapidapi else "&apikey={}".format( + self.key) + if self._append_type: + url = '{}{}&datatype={}'.format(url, apikey_parameter, oformat) + else: + url = '{}{}'.format(url, apikey_parameter) + return await self._handle_api_call(url), data_key, meta_data_key + return _call_wrapper + + @classmethod + def _output_format_sector(cls, func, override=None): + """ Decorator in charge of giving the output its right format, either + json or pandas (replacing the % for usable floats, range 0-1.0) + + Keyword Arguments: + func: The function to be decorated + override: Override the internal format of the call, default None + Returns: + A decorator for the format sector api call + """ + @wraps(func) + async def _format_wrapper(self, *args, **kwargs): + json_response, data_key, meta_data_key = await func( + self, *args, **kwargs) + if isinstance(data_key, list): + # Replace the strings into percentage + data = {key: {k: self.percentage_to_float(v) + for k, v in json_response[key].items()} for key in data_key} + else: + data = json_response[data_key] + # TODO: Fix orientation in a better way + meta_data = json_response[meta_data_key] + # Allow to override the output parameter in the call + if override is None: + output_format = self.output_format.lower() + elif 'json' or 'pandas' in override.lower(): + output_format = override.lower() + # Choose output format + if output_format == 'json': + return data, meta_data + elif output_format == 'pandas': + data_pandas = pandas.DataFrame.from_dict(data, + orient='columns') + # Rename columns to have a nicer name + col_names = [re.sub(r'\d+.', '', name).strip(' ') + for name in list(data_pandas)] + data_pandas.columns = col_names + return data_pandas, meta_data + else: + raise ValueError('Format: {} is not supported'.format( + self.output_format)) + return _format_wrapper + + @classmethod + def _output_format(cls, func, override=None): + """ Decorator in charge of giving the output its right format, either + json or pandas + + Keyword Arguments: + func: The function to be decorated + override: Override the internal format of the call, default None + """ + @wraps(func) + async def _format_wrapper(self, *args, **kwargs): + call_response, data_key, meta_data_key = await func( + self, *args, **kwargs) + if 'json' in self.output_format.lower() or 'pandas' \ + in self.output_format.lower(): + data = call_response[data_key] + + if meta_data_key is not None: + meta_data = call_response[meta_data_key] + else: + meta_data = None + # Allow to override the output parameter in the call + if override is None: + output_format = self.output_format.lower() + elif 'json' or 'pandas' in override.lower(): + output_format = override.lower() + # Choose output format + if output_format == 'json': + return data, meta_data + elif output_format == 'pandas': + if isinstance(data, list): + # If the call returns a list, then we will append them + # in the resulting data frame. If in the future + # alphavantage decides to do more with returning arrays + # this might become buggy. For now will do the trick. + data_array = [] + for val in data: + data_array.append([v for _, v in val.items()]) + data_pandas = pandas.DataFrame(data_array, columns=[ + k for k, _ in data[0].items()]) + else: + try: + data_pandas = pandas.DataFrame.from_dict(data, + orient='index', + dtype='float') + # This is for Global quotes or any other new Alpha Vantage + # data that is added. + # It will have to be updated so that we can get exactly + # The dataframes we want moving forward + except ValueError: + data = {data_key: data} + data_pandas = pandas.DataFrame.from_dict(data, + orient='index', + dtype='object') + return data_pandas, meta_data + + if 'integer' in self.indexing_type: + # Set Date as an actual column so a new numerical index + # will be created, but only when specified by the user. + data_pandas.reset_index(level=0, inplace=True) + data_pandas.index.name = 'index' + else: + data_pandas.index.name = 'date' + # convert to pandas._libs.tslibs.timestamps.Timestamp + data_pandas.index = pandas.to_datetime( + data_pandas.index) + return data_pandas, meta_data + elif 'csv' in self.output_format.lower(): + return call_response, None + else: + raise ValueError('Format: {} is not supported'.format( + self.output_format)) + return _format_wrapper + + def set_proxy(self, proxy=None): + """ + Set a new proxy configuration + + Keyword Arguments: + proxy: String URL of the proxy. + """ + self.proxy = proxy or '' + + async def _handle_api_call(self, url): + """ + Handle the return call from the api and return a data and meta_data + object. It raises a ValueError on problems + + Keyword Arguments: + url: The url of the service + """ + if not self.session: + self.session = aiohttp.ClientSession() + response = await self.session.get(url, proxy=self.proxy, headers=self.headers) + if 'json' in self.output_format.lower() or 'pandas' in \ + self.output_format.lower(): + json_response = await response.json() + if not json_response: + raise ValueError( + 'Error getting data from the api, no return was given.') + elif "Error Message" in json_response: + raise ValueError(json_response["Error Message"]) + elif "Information" in json_response and self.treat_info_as_error: + raise ValueError(json_response["Information"]) + elif "Note" in json_response and self.treat_info_as_error: + raise ValueError(json_response["Note"]) + return json_response + else: + csv_response = csv.reader(response.text.splitlines()) + if not csv_response: + raise ValueError( + 'Error getting data from the api, no return was given.') + return csv_response + + async def close(self): + """ + Close the underlying aiohttp session + """ + if self.session and not self.session.closed: + await self.session.close() diff --git a/alpha_vantage/async_support/cryptocurrencies.py b/alpha_vantage/async_support/cryptocurrencies.py new file mode 120000 index 0000000..9373561 --- /dev/null +++ b/alpha_vantage/async_support/cryptocurrencies.py @@ -0,0 +1 @@ +../cryptocurrencies.py \ No newline at end of file diff --git a/alpha_vantage/async_support/foreignexchange.py b/alpha_vantage/async_support/foreignexchange.py new file mode 120000 index 0000000..e2e55fd --- /dev/null +++ b/alpha_vantage/async_support/foreignexchange.py @@ -0,0 +1 @@ +../foreignexchange.py \ No newline at end of file diff --git a/alpha_vantage/async_support/sectorperformance.py b/alpha_vantage/async_support/sectorperformance.py new file mode 120000 index 0000000..a9539d6 --- /dev/null +++ b/alpha_vantage/async_support/sectorperformance.py @@ -0,0 +1 @@ +../sectorperformance.py \ No newline at end of file diff --git a/alpha_vantage/async_support/techindicators.py b/alpha_vantage/async_support/techindicators.py new file mode 120000 index 0000000..408eb33 --- /dev/null +++ b/alpha_vantage/async_support/techindicators.py @@ -0,0 +1 @@ +../techindicators.py \ No newline at end of file diff --git a/alpha_vantage/async_support/timeseries.py b/alpha_vantage/async_support/timeseries.py new file mode 120000 index 0000000..1e21a60 --- /dev/null +++ b/alpha_vantage/async_support/timeseries.py @@ -0,0 +1 @@ +../timeseries.py \ No newline at end of file diff --git a/alpha_vantage/sectorperformance.py b/alpha_vantage/sectorperformance.py index 411f5ab..ad8a0c2 100644 --- a/alpha_vantage/sectorperformance.py +++ b/alpha_vantage/sectorperformance.py @@ -1,11 +1,5 @@ #!/usr/bin/env python from .alphavantage import AlphaVantage as av -from functools import wraps -try: - import pandas -except ImportError: - pass -import re class SectorPerformances(av): @@ -30,50 +24,7 @@ def percentage_to_float(self, val): """ return float(val.strip('%')) / 100 - def _output_format_sector(func, override=None): - """ Decorator in charge of giving the output its right format, either - json or pandas (replacing the % for usable floats, range 0-1.0) - - Keyword Arguments: - func: The function to be decorated - override: Override the internal format of the call, default None - Returns: - A decorator for the format sector api call - """ - @wraps(func) - def _format_wrapper(self, *args, **kwargs): - json_response, data_key, meta_data_key = func( - self, *args, **kwargs) - if isinstance(data_key, list): - # Replace the strings into percentage - data = {key: {k: self.percentage_to_float(v) - for k, v in json_response[key].items()} for key in data_key} - else: - data = json_response[data_key] - # TODO: Fix orientation in a better way - meta_data = json_response[meta_data_key] - # Allow to override the output parameter in the call - if override is None: - output_format = self.output_format.lower() - elif 'json' or 'pandas' in override.lower(): - output_format = override.lower() - # Choose output format - if output_format == 'json': - return data, meta_data - elif output_format == 'pandas': - data_pandas = pandas.DataFrame.from_dict(data, - orient='columns') - # Rename columns to have a nicer name - col_names = [re.sub(r'\d+.', '', name).strip(' ') - for name in list(data_pandas)] - data_pandas.columns = col_names - return data_pandas, meta_data - else: - raise ValueError('Format: {} is not supported'.format( - self.output_format)) - return _format_wrapper - - @_output_format_sector + @av._output_format_sector @av._call_api_on_func def get_sector(self): """This API returns the realtime and historical sector performances diff --git a/alpha_vantage/techindicators.py b/alpha_vantage/techindicators.py index f6508a3..ccb0b9d 100644 --- a/alpha_vantage/techindicators.py +++ b/alpha_vantage/techindicators.py @@ -801,7 +801,10 @@ def get_bbands(self, symbol, interval='daily', time_period=20, series_type='clo symbol: the symbol for the equity we want to get its data interval: time interval between two conscutive values, supported values are '1min', '5min', '15min', '30min', '60min', 'daily', - 'weekly', 'monthly' (default 'daily' + 'weekly', 'monthly' (default 'daily') + time_period: Number of data points used to calculate each BBANDS value. + Positive integers are accepted (e.g., time_period=60, time_period=200) + (default=20) series_type: The desired price type in the time series. Four types are supported: 'close', 'open', 'high', 'low' (default 'close') nbdevup: The standard deviation multiplier of the upper band. Positive diff --git a/alpha_vantage/timeseries.py b/alpha_vantage/timeseries.py index a22642f..045fcd2 100644 --- a/alpha_vantage/timeseries.py +++ b/alpha_vantage/timeseries.py @@ -111,18 +111,6 @@ def get_monthly_adjusted(self, symbol): _FUNCTION_KEY = "TIME_SERIES_MONTHLY_ADJUSTED" return _FUNCTION_KEY, 'Monthly Adjusted Time Series', 'Meta Data' - @av._output_format - @av._call_api_on_func - def get_batch_stock_quotes(self, symbols): - """ Return multiple stock quotes with a single request. - It raises ValueError when problems arise - - Keyword Arguments: - symbols: A tuple or list Sof symbols to query - """ - _FUNCTION_KEY = "BATCH_STOCK_QUOTES" - return _FUNCTION_KEY, 'Stock Quotes', 'Meta Data' - @av._output_format @av._call_api_on_func def get_quote_endpoint(self, symbol): diff --git a/docs/conf.py b/docs/conf.py index ac50ceb..4974f39 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -55,9 +55,9 @@ # built documents. # # The short X.Y version. -version = u'2.1.3' +version = u'2.2.0' # The full version, including alpha/beta/rc tags. -release = u'2.1.3' +release = u'2.2.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/index.rst b/docs/index.rst index 5558dc4..affba53 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -82,10 +82,9 @@ To find out more about the available api calls, visit the alpha-vantage documentation at http://www.alphavantage.co/documentation/ - .. |Build Status| image:: https://travis-ci.org/RomelTorres/alpha_vantage.png?branch=master :target: https://travis-ci.org/RomelTorres/alpha_vantage -.. |PyPI version| image:: https://badge.fury.io/py/alpha_vantage.svg +.. |PyPI version| image:: https://badge.fury.io/py/alpha-vantage.svg :target: https://badge.fury.io/py/alpha_vantage .. |Documentation Status| image:: https://readthedocs.org/projects/alpha-vantage/badge/?version=latest :target: http://alpha-vantage.readthedocs.io/en/latest/?badge=latest diff --git a/setup.py b/setup.py index 1e58a6a..a29a174 100644 --- a/setup.py +++ b/setup.py @@ -11,14 +11,14 @@ setup( name='alpha_vantage', - version='2.1.3', + version='2.2.0', author='Romel J. Torres', author_email='romel.torres@gmail.com', license='MIT', description='Python module to get stock data from the Alpha Vantage Api', long_description=long_description, classifiers=[ - 'Development Status :: 4 - Beta', + 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Topic :: Office/Business :: Financial :: Investment', 'License :: OSI Approved :: MIT License', @@ -26,12 +26,16 @@ 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8' ], url='https://github.com/RomelTorres/alpha_vantage', install_requires=[ + 'aiohttp', 'requests' ], test_requires=[ + 'aioresponses', 'nose', 'requests_mock' ], diff --git a/test_alpha_vantage/test_alphavantage.py b/test_alpha_vantage/test_alphavantage.py index e72d3da..624254c 100644 --- a/test_alpha_vantage/test_alphavantage.py +++ b/test_alpha_vantage/test_alphavantage.py @@ -1,14 +1,15 @@ +#! /usr/bin/env python from ..alpha_vantage.alphavantage import AlphaVantage from ..alpha_vantage.timeseries import TimeSeries from ..alpha_vantage.techindicators import TechIndicators from ..alpha_vantage.sectorperformance import SectorPerformances -from ..alpha_vantage.cryptocurrencies import CryptoCurrencies from ..alpha_vantage.foreignexchange import ForeignExchange + from pandas import DataFrame as df, Timestamp + import unittest import sys from os import path -import requests import requests_mock @@ -199,30 +200,3 @@ def test_foreign_exchange(self, mock_request): from_currency='BTC', to_currency='CNY') self.assertIsInstance( data, dict, 'Result Data must be a dictionary') - - @requests_mock.Mocker() - def test_batch_quotes(self, mock_request): - """ Test that api call returns a json file as requested - """ - ts = TimeSeries(key=TestAlphaVantage._API_KEY_TEST) - url = "https://www.alphavantage.co/query?function=BATCH_STOCK_QUOTES&symbols=MSFT,FB,AAPL&apikey=test" - path_file = self.get_file_from_url("mock_batch_quotes") - with open(path_file) as f: - mock_request.get(url, text=f.read()) - data, _ = ts.get_batch_stock_quotes(symbols=('MSFT', 'FB', 'AAPL')) - self.assertIsInstance( - data[0], dict, 'Result Data must be a json dictionary') - - @requests_mock.Mocker() - def test_batch_quotes_pandas(self, mock_request): - """ Test that api call returns a json file as requested - """ - ts = TimeSeries(key=TestAlphaVantage._API_KEY_TEST, - output_format='pandas') - url = "https://www.alphavantage.co/query?function=BATCH_STOCK_QUOTES&symbols=MSFT,FB,AAPL&apikey=test&datatype=json" - path_file = self.get_file_from_url("mock_batch_quotes") - with open(path_file) as f: - mock_request.get(url, text=f.read()) - data, _ = ts.get_batch_stock_quotes(symbols=('MSFT', 'FB', 'AAPL')) - self.assertIsInstance( - data, df, 'Result Data must be a pandas dataframe') diff --git a/test_alpha_vantage/test_alphavantage_async.py b/test_alpha_vantage/test_alphavantage_async.py new file mode 100644 index 0000000..a9853b1 --- /dev/null +++ b/test_alpha_vantage/test_alphavantage_async.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python +from ..alpha_vantage.async_support.alphavantage import AlphaVantage +from ..alpha_vantage.async_support.timeseries import TimeSeries +from ..alpha_vantage.async_support.techindicators import TechIndicators +from ..alpha_vantage.async_support.sectorperformance import SectorPerformances +from ..alpha_vantage.async_support.foreignexchange import ForeignExchange + +from pandas import DataFrame as df, Timestamp + +import asyncio +from aioresponses import aioresponses +from functools import wraps +import json +from os import path +import unittest + + +def make_async(f): + @wraps(f) + def test_wrapper(*args, **kwargs): + coro = asyncio.coroutine(f) + future = coro(*args, **kwargs) + asyncio.get_event_loop().run_until_complete(future) + return test_wrapper + + +class TestAlphaVantageAsync(unittest.TestCase): + """ + Async local tests for AlphaVantage components + """ + _API_KEY_TEST = "test" + _API_EQ_NAME_TEST = 'MSFT' + + @staticmethod + def get_file_from_url(url): + """ + Return the file name used for testing, found in the test data folder + formed using the original url + """ + tmp = url + for ch in [':', '/', '.', '?', '=', '&', ',']: + if ch in tmp: + tmp = tmp.replace(ch, '_') + path_dir = path.join(path.dirname( + path.abspath(__file__)), 'test_data/') + return path.join(path.join(path_dir, tmp)) + + def test_key_none(self): + """ + Raise an error when a key has not been given + """ + try: + AlphaVantage() + self.fail(msg='A None api key must raise an error') + except ValueError: + self.assertTrue(True) + + @make_async + async def test_handle_api_call(self): + """ + Test that api call returns a json file as requested + """ + av = AlphaVantage(key=TestAlphaVantageAsync._API_KEY_TEST) + url = "https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=MSFT&interval=1min&apikey=test" + path_file = self.get_file_from_url("mock_time_series") + with open(path_file) as f, aioresponses() as m: + m.get(url, payload=json.loads(f.read())) + data = await av._handle_api_call(url) + self.assertIsInstance( + data, dict, 'Result Data must be a dictionary') + await av.close() + + @make_async + async def test_rapidapi_key(self): + """ + Test that the rapidAPI key calls the rapidAPI endpoint + """ + ts = TimeSeries(key=TestAlphaVantageAsync._API_KEY_TEST, rapidapi=True) + url = "https://alpha-vantage.p.rapidapi.com/query?function=TIME_SERIES_INTRADAY&symbol=MSFT&interval=1min&outputsize=full&datatype=json" + path_file = self.get_file_from_url("mock_time_series") + with open(path_file) as f, aioresponses() as m: + m.get(url, payload=json.loads(f.read())) + data, _ = await ts.get_intraday( + "MSFT", interval='1min', outputsize='full') + self.assertIsInstance( + data, dict, 'Result Data must be a dictionary') + await ts.close() + + @make_async + async def test_time_series_intraday(self): + """ + Test that api call returns a json file as requested + """ + ts = TimeSeries(key=TestAlphaVantageAsync._API_KEY_TEST) + url = "https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=MSFT&interval=1min&outputsize=full&apikey=test&datatype=json" + path_file = self.get_file_from_url("mock_time_series") + with open(path_file) as f, aioresponses() as m: + m.get(url, payload=json.loads(f.read())) + data, _ = await ts.get_intraday( + "MSFT", interval='1min', outputsize='full') + self.assertIsInstance( + data, dict, 'Result Data must be a dictionary') + await ts.close() + + @make_async + async def test_time_series_intraday_pandas(self): + """ + Test that api call returns a json file as requested + """ + ts = TimeSeries(key=TestAlphaVantageAsync._API_KEY_TEST, + output_format='pandas') + url = "https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=MSFT&interval=1min&outputsize=full&apikey=test&datatype=json" + path_file = self.get_file_from_url("mock_time_series") + with open(path_file) as f, aioresponses() as m: + m.get(url, payload=json.loads(f.read())) + data, _ = await ts.get_intraday( + "MSFT", interval='1min', outputsize='full') + self.assertIsInstance( + data, df, 'Result Data must be a pandas data frame') + await ts.close() + + @make_async + async def test_time_series_intraday_date_indexing(self): + """ + Test that api call returns a pandas data frame with a date as index + """ + ts = TimeSeries(key=TestAlphaVantageAsync._API_KEY_TEST, + output_format='pandas', indexing_type='date') + url = "https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=MSFT&interval=1min&outputsize=full&apikey=test&datatype=json" + path_file = self.get_file_from_url("mock_time_series") + with open(path_file) as f, aioresponses() as m: + m.get(url, payload=json.loads(f.read())) + data, _ = await ts.get_intraday( + "MSFT", interval='1min', outputsize='full') + if ts.indexing_type == 'date': + assert isinstance(data.index[0], Timestamp) + else: + assert isinstance(data.index[0], str) + await ts.close() + + @make_async + async def test_time_series_intraday_date_integer(self): + """ + Test that api call returns a pandas data frame with an integer as index + """ + ts = TimeSeries(key=TestAlphaVantageAsync._API_KEY_TEST, + output_format='pandas', indexing_type='integer') + url = "https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=MSFT&interval=1min&outputsize=full&apikey=test&datatype=json" + path_file = self.get_file_from_url("mock_time_series") + with open(path_file) as f, aioresponses() as m: + m.get(url, payload=json.loads(f.read())) + data, _ = await ts.get_intraday( + "MSFT", interval='1min', outputsize='full') + assert type(data.index[0]) == int + await ts.close() + + @make_async + async def test_technical_indicator_sma_python3(self): + """ + Test that api call returns a json file as requested + """ + ti = TechIndicators(key=TestAlphaVantageAsync._API_KEY_TEST) + url = "https://www.alphavantage.co/query?function=SMA&symbol=MSFT&interval=15min&time_period=10&series_type=close&apikey=test" + path_file = self.get_file_from_url("mock_technical_indicator") + with open(path_file) as f, aioresponses() as m: + m.get(url, payload=json.loads(f.read())) + data, _ = await ti.get_sma("MSFT", interval='15min', + time_period=10, series_type='close') + self.assertIsInstance( + data, dict, 'Result Data must be a dictionary') + await ti.close() + + @make_async + async def test_technical_indicator_sma_pandas(self): + """ + Test that api call returns a json file as requested + """ + ti = TechIndicators( + key=TestAlphaVantageAsync._API_KEY_TEST, output_format='pandas') + url = "https://www.alphavantage.co/query?function=SMA&symbol=MSFT&interval=15min&time_period=10&series_type=close&apikey=test" + path_file = self.get_file_from_url("mock_technical_indicator") + with open(path_file) as f, aioresponses() as m: + m.get(url, payload=json.loads(f.read())) + data, _ = await ti.get_sma("MSFT", interval='15min', + time_period=10, series_type='close') + self.assertIsInstance( + data, df, 'Result Data must be a pandas data frame') + await ti.close() + + @make_async + async def test_sector_perfomance_python3(self): + """ + Test that api call returns a json file as requested + """ + sp = SectorPerformances(key=TestAlphaVantageAsync._API_KEY_TEST) + url = "https://www.alphavantage.co/query?function=SECTOR&apikey=test" + path_file = self.get_file_from_url("mock_sector") + with open(path_file) as f, aioresponses() as m: + m.get(url, payload=json.loads(f.read())) + data, _ = await sp.get_sector() + self.assertIsInstance( + data, dict, 'Result Data must be a dictionary') + await sp.close() + + @make_async + async def test_sector_perfomance_pandas(self): + """ + Test that api call returns a json file as requested + """ + sp = SectorPerformances( + key=TestAlphaVantageAsync._API_KEY_TEST, output_format='pandas') + url = "https://www.alphavantage.co/query?function=SECTOR&apikey=test" + path_file = self.get_file_from_url("mock_sector") + with open(path_file) as f, aioresponses() as m: + m.get(url, payload=json.loads(f.read())) + data, _ = await sp.get_sector() + self.assertIsInstance( + data, df, 'Result Data must be a pandas data frame') + await sp.close() + + @make_async + async def test_foreign_exchange(self): + """ + Test that api call returns a json file as requested + """ + fe = ForeignExchange(key=TestAlphaVantageAsync._API_KEY_TEST) + url = "https://www.alphavantage.co/query?function=CURRENCY_EXCHANGE_RATE&from_currency=BTC&to_currency=CNY&apikey=test" + path_file = self.get_file_from_url("mock_foreign_exchange") + with open(path_file) as f, aioresponses() as m: + m.get(url, payload=json.loads(f.read())) + data, _ = await fe.get_currency_exchange_rate( + from_currency='BTC', to_currency='CNY') + self.assertIsInstance( + data, dict, 'Result Data must be a dictionary') + await fe.close() diff --git a/test_alpha_vantage/test_data/mock_batch_quotes b/test_alpha_vantage/test_data/mock_batch_quotes deleted file mode 100644 index 7d9e033..0000000 --- a/test_alpha_vantage/test_data/mock_batch_quotes +++ /dev/null @@ -1,27 +0,0 @@ -{ - "Meta Data": { - "1. Information": "Batch Stock Market Quotes", - "2. Notes": "IEX Real-Time Price provided for free by IEX (https://iextrading.com/developer/).", - "3. Time Zone": "US/Eastern" - }, - "Stock Quotes": [ - { - "1. symbol": "MSFT", - "2. price": "85.5900", - "3. volume": "--", - "4. timestamp": "2017-12-29 15:59:58" - }, - { - "1. symbol": "FB", - "2. price": "176.5400", - "3. volume": "--", - "4. timestamp": "2017-12-29 15:59:59" - }, - { - "1. symbol": "AAPL", - "2. price": "169.2800", - "3. volume": "--", - "4. timestamp": "2017-12-29 15:59:59" - } - ] -} diff --git a/test_alpha_vantage/test_integration_alphavantage.py b/test_alpha_vantage/test_integration_alphavantage.py index 20519a9..b52b7a1 100644 --- a/test_alpha_vantage/test_integration_alphavantage.py +++ b/test_alpha_vantage/test_integration_alphavantage.py @@ -7,9 +7,9 @@ from pandas import DataFrame as df +import os import unittest import timeit -import os import time diff --git a/test_alpha_vantage/test_integration_alphavantage_async.py b/test_alpha_vantage/test_integration_alphavantage_async.py new file mode 100644 index 0000000..31d2d96 --- /dev/null +++ b/test_alpha_vantage/test_integration_alphavantage_async.py @@ -0,0 +1,298 @@ +#!/usr/bin/env python +from ..alpha_vantage.async_support.alphavantage import AlphaVantage +from ..alpha_vantage.async_support.timeseries import TimeSeries +from ..alpha_vantage.async_support.techindicators import TechIndicators +from ..alpha_vantage.async_support.cryptocurrencies import CryptoCurrencies +from ..alpha_vantage.async_support.foreignexchange import ForeignExchange + +from functools import wraps +from pandas import DataFrame as df + +import asyncio +import os +import time +import timeit +import unittest + + +def make_async(f): + @wraps(f) + def test_wrapper(*args, **kwargs): + coro = asyncio.coroutine(f) + future = coro(*args, **kwargs) + asyncio.get_event_loop().run_until_complete(future) + return test_wrapper + + +class TestAlphaVantageAsync(unittest.TestCase): + """ + Test data request different implementations + """ + _API_KEY_TEST = os.environ['API_KEY'] + _API_EQ_NAME_TEST = 'MSFT' + _RAPIDAPI_KEY_TEST = os.getenv("RAPIDAPI_KEY") + + def setUp(self): + """ + Wait some time before running each call again. + """ + time.sleep(1) + + async def _assert_result_is_format(self, func, output_format='json', **args): + """Check that the data and meta data object are dictionaries + + Keyword arguments + func -- the function to assert its format + output_format -- the format of the call + **args -- The parameters for the call + """ + stime = timeit.default_timer() + data, meta_data = await func(**args) + elapsed = timeit.default_timer() - stime + print('Function: {} - Format: {} - Took: {}'.format(func.__name__, + output_format, + elapsed)) + if output_format == 'json': + self.assertIsInstance( + data, dict, 'Result Data must be a dictionary') + if meta_data is not None: + self.assertIsInstance(meta_data, dict, 'Result Meta Data must be a \ + dictionary') + elif output_format == 'pandas': + self.assertIsInstance( + data, df, 'Result Data must be a pandas data frame') + if meta_data is not None: + self.assertIsInstance(meta_data, dict, 'Result Meta Data must be a \ + dictionary') + + def test_key_none(self): + """ + Raise an error when a key has not been given + """ + try: + AlphaVantage() + self.fail(msg='A None api key must raise an error') + except ValueError: + self.assertTrue(True) + + @make_async + async def test_rapidapi_key_with_get_daily(self): + """ + RapidAPI calls must return the same data as non-rapidapi calls + """ + # Test rapidAPI calls are the same as regular ones + ts_rapidapi = TimeSeries( + key=TestAlphaVantageAsync._RAPIDAPI_KEY_TEST, rapidapi=True) + ts = TimeSeries(key=TestAlphaVantageAsync._API_KEY_TEST) + rapidapi_data, rapidapi_metadata = ts_rapidapi.get_daily( + symbol=TestAlphaVantageAsync._API_EQ_NAME_TEST) + data, metadata = ts.get_daily( + symbol=TestAlphaVantageAsync._API_EQ_NAME_TEST) + self.assertTrue(rapidapi_data == data) + + @make_async + async def test_get_daily_is_format(self): + """ + Result must be a dictionary containing the json data + """ + # Test dictionary as output + ts = TimeSeries(key=TestAlphaVantageAsync._API_KEY_TEST) + await self._assert_result_is_format(ts.get_daily, + symbol=TestAlphaVantageAsync._API_EQ_NAME_TEST) + await ts.close() + # Test panda as output + ts = TimeSeries(key=TestAlphaVantageAsync._API_KEY_TEST, + output_format='pandas') + await self._assert_result_is_format(ts.get_daily, + output_format='pandas', + symbol=TestAlphaVantageAsync._API_EQ_NAME_TEST) + await ts.close() + + @make_async + async def test_get_daily_adjusted_is_format(self): + """ + Result must be a dictionary containing the json data + """ + # Test dictionary as output + ts = TimeSeries(key=TestAlphaVantageAsync._API_KEY_TEST) + await self._assert_result_is_format(ts.get_daily_adjusted, + symbol=TestAlphaVantageAsync._API_EQ_NAME_TEST) + await ts.close() + # Test panda as output + ts = TimeSeries(key=TestAlphaVantageAsync._API_KEY_TEST, + output_format='pandas') + await self._assert_result_is_format(ts.get_daily_adjusted, + output_format='pandas', + symbol=TestAlphaVantageAsync._API_EQ_NAME_TEST) + await ts.close() + + @make_async + async def test_get_sma_is_format(self): + """ + Result must be a dictionary containing the json data + """ + # Test dictionary as output + ti = TechIndicators(key=TestAlphaVantageAsync._API_KEY_TEST) + await self._assert_result_is_format(ti.get_sma, + symbol=TestAlphaVantageAsync._API_EQ_NAME_TEST) + await ti.close() + # Test panda as output + ti = TechIndicators( + key=TestAlphaVantageAsync._API_KEY_TEST, output_format='pandas') + await self._assert_result_is_format(ti.get_sma, output_format='pandas', + symbol=TestAlphaVantageAsync._API_EQ_NAME_TEST) + await ti.close() + + @make_async + async def test_get_ema_is_format(self): + """ + Result must be a dictionary containing the json data + """ + # Test dictionary as output + ti = TechIndicators(key=TestAlphaVantageAsync._API_KEY_TEST) + await self._assert_result_is_format(ti.get_ema, + symbol=TestAlphaVantageAsync._API_EQ_NAME_TEST) + await ti.close() + # Test panda as output + ti = TechIndicators( + key=TestAlphaVantageAsync._API_KEY_TEST, output_format='pandas') + await self._assert_result_is_format(ti.get_ema, + output_format='pandas', + symbol=TestAlphaVantageAsync._API_EQ_NAME_TEST) + await ti.close() + + @make_async + async def test_get_currency_exchange_rate(self): + """ + Test that we get a dictionary containing json data + """ + cc = ForeignExchange(key=TestAlphaVantageAsync._API_KEY_TEST) + await self._assert_result_is_format(cc.get_currency_exchange_rate, + output_format='json', + from_currency='USD', + to_currency='BTC') + await cc.close() + + @make_async + async def test_get_currency_exchange_intraday_json(self): + """ + Test that we get a dictionary containing json data + """ + fe = ForeignExchange(key=TestAlphaVantageAsync._API_KEY_TEST) + await self._assert_result_is_format(fe.get_currency_exchange_intraday, + output_format='json', + from_symbol='EUR', + to_symbol='USD', + interval='1min') + await fe.close() + + @make_async + async def test_get_currency_exchange_intraday_pandas(self): + """ + Test that we get a dictionary containing pandas data + """ + fe = ForeignExchange( + key=TestAlphaVantageAsync._API_KEY_TEST, output_format='pandas') + await self._assert_result_is_format(fe.get_currency_exchange_intraday, + output_format='pandas', + from_symbol='USD', + to_symbol='JPY', + interval='5min') + await fe.close() + + @make_async + async def test_get_currency_exchange_daily_json(self): + """ + Test that we get a dictionary containing json data + """ + fe = ForeignExchange(key=TestAlphaVantageAsync._API_KEY_TEST) + await self._assert_result_is_format(fe.get_currency_exchange_daily, + output_format='json', + from_symbol='EUR', + to_symbol='USD') + await fe.close() + + @make_async + async def test_get_currency_exchange_daily_pandas(self): + """ + Test that we get a dictionary containing pandas data + """ + fe = ForeignExchange( + key=TestAlphaVantageAsync._API_KEY_TEST, output_format='pandas') + await self._assert_result_is_format(fe.get_currency_exchange_daily, + output_format='pandas', + from_symbol='USD', + to_symbol='JPY') + await fe.close() + + @make_async + async def test_get_currency_exchange_weekly_json(self): + """ + Test that we get a dictionary containing json data + """ + fe = ForeignExchange(key=TestAlphaVantageAsync._API_KEY_TEST) + await self._assert_result_is_format(fe.get_currency_exchange_weekly, + output_format='json', + from_symbol='EUR', + to_symbol='USD', + outputsize='full') + await fe.close() + + @make_async + async def test_get_currency_exchange_weekly_pandas(self): + """ + Test that we get a dictionary containing pandas data + """ + fe = ForeignExchange( + key=TestAlphaVantageAsync._API_KEY_TEST, output_format='pandas') + await self._assert_result_is_format(fe.get_currency_exchange_weekly, + output_format='pandas', + from_symbol='USD', + to_symbol='JPY') + await fe.close() + + @make_async + async def test_get_currency_exchange_monthly_json(self): + """ + Test that we get a dictionary containing json data + """ + fe = ForeignExchange(key=TestAlphaVantageAsync._API_KEY_TEST) + await self._assert_result_is_format(fe.get_currency_exchange_monthly, + output_format='json', + from_symbol='EUR', + to_symbol='USD') + await fe.close() + + @make_async + async def test_get_currency_exchange_monthly_pandas(self): + """ + Test that we get a dictionary containing pandas data + """ + fe = ForeignExchange( + key=TestAlphaVantageAsync._API_KEY_TEST, output_format='pandas') + await self._assert_result_is_format(fe.get_currency_exchange_monthly, + output_format='pandas', + from_symbol='USD', + to_symbol='JPY', + outputsize='full') + await fe.close() + + @make_async + async def test_get_digital_currency_weekly(self): + """ + Test that we get a dictionary containing json data + """ + cc = CryptoCurrencies(key=TestAlphaVantageAsync._API_KEY_TEST) + await self._assert_result_is_format(cc.get_digital_currency_weekly, + output_format='json', + symbol='BTC', + market='CNY') + await cc.close() + # Test panda as output + cc = CryptoCurrencies( + key=TestAlphaVantageAsync._API_KEY_TEST, output_format='pandas') + await self._assert_result_is_format(cc.get_digital_currency_weekly, + output_format='pandas', + symbol='BTC', + market='CNY') + await cc.close()