diff --git a/README.md b/README.md index f99f428..665c7f5 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,35 @@ roll_up_capture(returns, window=60) Please [open an issue](https://github.com/quantopian/empyrical/issues/new) for support. +### Deprecated: Data Reading via `pandas-datareader` + +As of early 2018, Yahoo Finance has suffered major API breaks with no stable +replacement, and the Google Finance API has not been stable since late 2017 +[(source)](https://github.com/pydata/pandas-datareader/blob/da18fbd7621d473828d7fa81dfa5e0f9516b6793/README.rst). +In recent months it has become a greater and greater strain on the `empyrical` +development team to maintain support for fetching data through +`pandas-datareader` and other third-party libraries, as these APIs are known to +be unstable. + +As a result, all `empyrical` support for data reading functionality has been +deprecated and will be removed in a future version. + +Users should beware that the following functions are now deprecated: + +- `empyrical.utils.cache_dir` +- `empyrical.utils.data_path` +- `empyrical.utils.ensure_directory` +- `empyrical.utils.get_fama_french` +- `empyrical.utils.load_portfolio_risk_factors` +- `empyrical.utils.default_returns_func` +- `empyrical.utils.get_symbol_returns_from_yahoo` + +Users should expect regular failures from the following functions, pending +patches to the Yahoo or Google Finance API: + +- `empyrical.utils.default_returns_func` +- `empyrical.utils.get_symbol_returns_from_yahoo` + ## Contributing Please contribute using [Github Flow](https://guides.github.com/introduction/flow/). Create a branch, add commits, and [open a pull request](https://github.com/quantopian/empyrical/compare/). diff --git a/empyrical/__init__.py b/empyrical/__init__.py index 4ac64b0..3f88e79 100644 --- a/empyrical/__init__.py +++ b/empyrical/__init__.py @@ -55,6 +55,7 @@ roll_up_capture, roll_up_down_capture, sharpe_ratio, + simple_returns, sortino_ratio, stability_of_timeseries, tail_ratio, diff --git a/empyrical/deprecate.py b/empyrical/deprecate.py new file mode 100644 index 0000000..1778e16 --- /dev/null +++ b/empyrical/deprecate.py @@ -0,0 +1,45 @@ +"""Utilities for marking deprecated functions.""" +# Copyright 2018 Quantopian, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import warnings +from functools import wraps + + +def deprecated(msg=None, stacklevel=2): + """ + Used to mark a function as deprecated. + Parameters + ---------- + msg : str + The message to display in the deprecation warning. + stacklevel : int + How far up the stack the warning needs to go, before + showing the relevant calling lines. + Usage + ----- + @deprecated(msg='function_a is deprecated! Use function_b instead.') + def function_a(*args, **kwargs): + """ + def deprecated_dec(fn): + @wraps(fn) + def wrapper(*args, **kwargs): + warnings.warn( + msg or "Function %s is deprecated." % fn.__name__, + category=DeprecationWarning, + stacklevel=stacklevel + ) + return fn(*args, **kwargs) + return wrapper + return deprecated_dec diff --git a/empyrical/stats.py b/empyrical/stats.py index 5335550..3f8aa11 100644 --- a/empyrical/stats.py +++ b/empyrical/stats.py @@ -183,6 +183,32 @@ def annualization_factor(period, annualization): return factor +def simple_returns(prices): + """ + Compute simple returns from a timeseries of prices. + + Parameters + ---------- + prices : pd.Series, pd.DataFrame or np.ndarray + Prices of assets in wide-format, with assets as columns, + and indexed by datetimes. + + Returns + ------- + returns : array-like + Returns of assets in wide-format, with assets as columns, + and index coerced to be tz-aware. + """ + if isinstance(prices, (pd.DataFrame, pd.Series)): + out = prices.pct_change().iloc[1:] + else: + # Assume np.ndarray + out = np.diff(prices, axis=0) + np.divide(out, prices[:-1], out=out) + + return out + + def cum_returns(returns, starting_value=0, out=None): """ Compute cumulative returns from simple returns. diff --git a/empyrical/tests/test_stats.py b/empyrical/tests/test_stats.py index 6b1cb09..4b64e37 100644 --- a/empyrical/tests/test_stats.py +++ b/empyrical/tests/test_stats.py @@ -160,6 +160,17 @@ class TestStats(BaseTestCase): 'one': pd.Series(one, index=df_index_month), 'two': pd.Series(two, index=df_index_month)}) + @parameterized.expand([ + # Constant price implies zero returns, + # and linearly increasing prices imples returns like 1/n + (flat_line_1, [0.0] * (flat_line_1.shape[0] - 1)), + (pos_line, [np.inf] + [1/n for n in range(1, 999)]) + ]) + def test_simple_returns(self, prices, expected): + simple_returns = self.empyrical.simple_returns(prices) + assert_almost_equal(np.array(simple_returns), expected, 4) + self.assert_indexes_match(simple_returns, prices.iloc[1:]) + @parameterized.expand([ (empty_returns, 0, []), (mixed_returns, 0, [0.0, 0.01, 0.111, 0.066559, 0.08789, 0.12052, diff --git a/empyrical/utils.py b/empyrical/utils.py index 2d72ab6..ed1ea62 100644 --- a/empyrical/utils.py +++ b/empyrical/utils.py @@ -1,5 +1,5 @@ # -# Copyright 2016 Quantopian, Inc. +# Copyright 2018 Quantopian, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,8 +23,22 @@ from numpy.lib.stride_tricks import as_strided import pandas as pd from pandas.tseries.offsets import BDay -from pandas_datareader import data as web - +try: + from pandas_datareader import data as web +except ImportError as e: + msg = ("Unable to import pandas_datareader. Suppressing import error and " + "continuing. All data reading functionality will raise errors; but " + "has been deprecated and will be removed in a later version.") + warnings.warn(msg) +from .deprecate import deprecated + +DATAREADER_DEPRECATION_WARNING = \ + ("Yahoo and Google Finance have suffered large API breaks with no " + "stable replacement. As a result, any data reading functionality " + "in empyrical has been deprecated and will be removed in a future " + "version. See README.md for more details: " + "\n\n" + "\thttps://github.com/quantopian/pyfolio/blob/master/README.md") try: # fast versions import bottleneck as bn @@ -175,6 +189,7 @@ def _roll_pandas(func, window, *args, **kwargs): return pd.Series(data, index=type(args[0].index)(index_values)) +@deprecated(msg=DATAREADER_DEPRECATION_WARNING) def cache_dir(environ=environ): try: return environ['EMPYRICAL_CACHE_DIR'] @@ -189,10 +204,12 @@ def cache_dir(environ=environ): ) +@deprecated(msg=DATAREADER_DEPRECATION_WARNING) def data_path(name): return join(cache_dir(), name) +@deprecated(msg=DATAREADER_DEPRECATION_WARNING) def ensure_directory(path): """ Ensure that a directory named "path" exists. @@ -209,10 +226,12 @@ def get_utc_timestamp(dt): """ Returns the Timestamp/DatetimeIndex with either localized or converted to UTC. + Parameters ---------- dt : Timestamp/DatetimeIndex the date(s) to be converted + Returns ------- same type as input @@ -234,6 +253,7 @@ def _1_bday_ago(): return pd.Timestamp.now().normalize() - _1_bday +@deprecated(msg=DATAREADER_DEPRECATION_WARNING) def get_fama_french(): """ Retrieve Fama-French factors via pandas-datareader @@ -257,6 +277,7 @@ def get_fama_french(): return five_factors +@deprecated(msg=DATAREADER_DEPRECATION_WARNING) def get_returns_cached(filepath, update_func, latest_dt, **kwargs): """ Get returns from a cached file if the cache is recent enough, @@ -324,6 +345,7 @@ def get_returns_cached(filepath, update_func, latest_dt, **kwargs): return returns +@deprecated(msg=DATAREADER_DEPRECATION_WARNING) def load_portfolio_risk_factors(filepath_prefix=None, start=None, end=None): """ Load risk factors Mkt-Rf, SMB, HML, Rf, and UMD. @@ -353,6 +375,7 @@ def load_portfolio_risk_factors(filepath_prefix=None, start=None, end=None): return five_factors.loc[start:end] +@deprecated(msg=DATAREADER_DEPRECATION_WARNING) def get_treasury_yield(start=None, end=None, period='3MO'): """ Load treasury yields from FRED. @@ -386,6 +409,7 @@ def get_treasury_yield(start=None, end=None, period='3MO'): return treasury +@deprecated(msg=DATAREADER_DEPRECATION_WARNING) def get_symbol_returns_from_yahoo(symbol, start=None, end=None): """ Wrapper for pandas.io.data.get_data_yahoo(). @@ -424,6 +448,7 @@ def get_symbol_returns_from_yahoo(symbol, start=None, end=None): return rets +@deprecated(msg=DATAREADER_DEPRECATION_WARNING) def default_returns_func(symbol, start=None, end=None): """ Gets returns for a symbol.