From 89f7060a804e29cc84f31713c81925cc3d711acd Mon Sep 17 00:00:00 2001 From: Frederic Fortier Date: Wed, 3 Jan 2018 17:01:23 -0500 Subject: [PATCH] BUG: created an empyrical patch for issue #126 --- catalyst/finance/risk/cumulative.py | 55 +- catalyst/patches/stats.py | 1112 +++++++++++++++++++++++++++ catalyst/support/issue_126.py | 34 + 3 files changed, 1185 insertions(+), 16 deletions(-) create mode 100644 catalyst/patches/stats.py create mode 100644 catalyst/support/issue_126.py diff --git a/catalyst/finance/risk/cumulative.py b/catalyst/finance/risk/cumulative.py index 37bd349b..b04b67a3 100644 --- a/catalyst/finance/risk/cumulative.py +++ b/catalyst/finance/risk/cumulative.py @@ -27,15 +27,15 @@ from .risk import ( choose_treasury ) -from empyrical import ( +from catalyst.patches.stats import ( alpha_beta_aligned, annual_volatility, - cum_returns, downside_risk, information_ratio, max_drawdown, sharpe_ratio, sortino_ratio, + cum_returns, ) import warnings from catalyst.constants import LOG_LEVEL @@ -161,9 +161,13 @@ class RiskMetricsCumulative(object): if len(self.algorithm_returns) == 1: self.algorithm_returns = np.append(0.0, self.algorithm_returns) - self.algorithm_cumulative_returns[dt_loc] = cum_returns( - self.algorithm_returns - )[-1] + try: + self.algorithm_cumulative_returns[dt_loc] = cum_returns( + self.algorithm_returns + )[-1] + except Exception as e: + log.debug('unable to calculate cum returns: {}'.format(e)) + self.algorithm_cumulative_returns[dt_loc] = np.nan algo_cumulative_returns_to_date = \ self.algorithm_cumulative_returns[:dt_loc + 1] @@ -196,8 +200,11 @@ class RiskMetricsCumulative(object): self.benchmark_cumulative_returns[dt_loc] = cum_returns( self.benchmark_returns )[-1] - except Exception: - self.benchmark_cumulative_returns[dt_loc] = 0 + except Exception as e: + log.debug( + 'unable to calculate benchmark cum returns: {}'.format(e) + ) + self.benchmark_cumulative_returns[dt_loc] = np.nan benchmark_cumulative_returns_to_date = \ self.benchmark_cumulative_returns[:dt_loc + 1] @@ -269,9 +276,16 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}" self.sharpe[dt_loc] = sharpe_ratio( self.algorithm_returns, ) - self.downside_risk[dt_loc] = downside_risk( - self.algorithm_returns - ) + + try: + self.downside_risk[dt_loc] = downside_risk( + self.algorithm_returns + ) + except Exception as e: + log.debug( + 'unable to calculate downside risk returns: {}'.format(e) + ) + self.downside_risk[dt_loc] = np.nan try: risk = self.downside_risk[dt_loc] @@ -279,17 +293,26 @@ algorithm_returns ({algo_count}) in range {start} : {end} on {dt}" self.algorithm_returns, _downside_risk=risk ) - except Exception: - # TODO: what causes it to error out? - self.sortino[dt_loc] = 0 + except Exception as e: + log.debug( + 'unable to calculate benchmark cum returns: {}'.format(e) + ) + self.sortino[dt_loc] = np.nan self.information[dt_loc] = information_ratio( self.algorithm_returns, self.benchmark_returns, ) - self.max_drawdown = max_drawdown( - self.algorithm_returns - ) + try: + self.max_drawdown = max_drawdown( + self.algorithm_returns + ) + except Exception as e: + log.debug( + 'unable to calculate max drawdown: {}'.format(e) + ) + self.max_drawdown = np.nan + self.max_drawdowns[dt_loc] = self.max_drawdown self.max_leverage = self.calculate_max_leverage() self.max_leverages[dt_loc] = self.max_leverage diff --git a/catalyst/patches/stats.py b/catalyst/patches/stats.py new file mode 100644 index 00000000..ac8b8688 --- /dev/null +++ b/catalyst/patches/stats.py @@ -0,0 +1,1112 @@ +# +# Copyright 2016 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. + +from __future__ import division + +from functools import wraps + +import pandas as pd +import numpy as np +from scipy import stats +from six import iteritems + +from empyrical.utils import nanmean, nanstd, nanmin + +APPROX_BDAYS_PER_MONTH = 21 +APPROX_BDAYS_PER_YEAR = 252 + +MONTHS_PER_YEAR = 12 +WEEKS_PER_YEAR = 52 + +DAILY = 'daily' +WEEKLY = 'weekly' +MONTHLY = 'monthly' +YEARLY = 'yearly' + +ANNUALIZATION_FACTORS = { + DAILY: APPROX_BDAYS_PER_YEAR, + WEEKLY: WEEKS_PER_YEAR, + MONTHLY: MONTHS_PER_YEAR, + YEARLY: 1 +} + + +def _adjust_returns(returns, adjustment_factor): + """ + Returns the returns series adjusted by adjustment_factor. Optimizes for the + case of adjustment_factor being 0 by returning returns itself, not a copy! + + Parameters + ---------- + returns : pd.Series or np.ndarray + adjustment_factor : pd.Series or np.ndarray or float or int + + Returns + ------- + pd.Series or np.ndarray + """ + if isinstance(adjustment_factor, (float, int)) and adjustment_factor == 0: + return returns + return returns - adjustment_factor + + +def annualization_factor(period, annualization): + """ + Return annualization factor from period entered or if a custom + value is passed in. + + Parameters + ---------- + period : str, optional + Defines the periodicity of the 'returns' data for purposes of + annualizing. Value ignored if `annualization` parameter is specified. + Defaults are: + 'monthly':12 + 'weekly': 52 + 'daily': 252 + annualization : int, optional + Used to suppress default values available in `period` to convert + returns into annual returns. Value should be the annual frequency of + `returns`. + + Returns + ------- + float + Annualization factor. + """ + if annualization is None: + try: + factor = ANNUALIZATION_FACTORS[period] + except KeyError: + raise ValueError( + "Period cannot be '{}'. " + "Can be '{}'.".format( + period, "', '".join(ANNUALIZATION_FACTORS.keys()) + ) + ) + else: + factor = annualization + return factor + + +def cum_returns(returns, starting_value=0): + """ + Compute cumulative returns from simple returns. + + Parameters + ---------- + returns : pd.Series or np.ndarray + Returns of the strategy as a percentage, noncumulative. + - Time series with decimal returns. + - Example: + 2015-07-16 -0.012143 + 2015-07-17 0.045350 + 2015-07-20 0.030957 + 2015-07-21 0.004902. + starting_value : float, optional + The starting returns. + + Returns + ------- + pd.Series or np.ndarray + Series of cumulative returns. + + Notes + ----- + For increased numerical accuracy, convert input to log returns + where it is possible to sum instead of multiplying. + PI((1+r_i)) - 1 = exp(ln(PI(1+r_i))) # x = exp(ln(x)) + = exp(SIGMA(ln(1+r_i)) # ln(a*b) = ln(a) + ln(b) + """ + # df_price.pct_change() adds a nan in first position, we can use + # that to have cum_logarithmic_returns start at the origin so that + # df_cum.iloc[0] == starting_value + # Note that we can't add that ourselves as we don't know which dt + # to use. + + if len(returns) < 1: + return type(returns)([]) + + if np.any(np.isnan(returns)): + returns = returns.copy() + returns[np.isnan(returns)] = 0. + + df_cum = (returns + 1).cumprod(axis=0) + + if starting_value == 0: + return df_cum - 1 + else: + return df_cum * starting_value + + +def cum_returns_final(returns, starting_value=0): + """ + Compute total returns from simple returns. + + Parameters + ---------- + returns : pd.Series or np.ndarray + Returns of the strategy as a percentage, noncumulative. + - Time series with decimal returns. + - Example: + 2015-07-16 -0.012143 + 2015-07-17 0.045350 + 2015-07-20 0.030957 + 2015-07-21 0.004902. + starting_value : float, optional + The starting returns. + + Returns + ------- + float + + """ + + if len(returns) == 0: + return np.nan + + return cum_returns(np.asanyarray(returns), + starting_value=starting_value)[-1] + + +def array_wrap(arg_name, _not_specified=object()): + """ + Decorator for functions working on array_likes that ensures the type of + output matches that of the input, delegating to the input's __array_wrap__. + + Parameters + ---------- + arg_name : str + + The name of the array_like arg to the wrapped function. Should be the + first positional parameter to the wrapped function. + + """ + + def dec(f): + @wraps(f) + def _wrapit(*args, **kwds): + obj = kwds.get(arg_name, _not_specified) + if obj is _not_specified: + obj = args[0] + + try: + wrap = obj.__array_wrap__ + except AttributeError: + wrap = None + result = f(*args, **kwds) + if wrap: + if not isinstance(result, np.ndarray): + result = np.asarray(result) + result = wrap(result) + return result + + return _wrapit + + return dec + + +@array_wrap('a') +def nancumsum(a, axis=None, dtype=None): + """ + Return the cumulative sum of array elements over a given axis treating Not + a Numbers (NaNs) as zero. The cumulative sum does not change when NaNs are + encountered and leading NaNs are replaced by zeros. + + Handles a subset of the edge cases handled by the nancumsum added in numpy + 1.12.0. + + Parameters + ---------- + a : np.ndarray or pd.Series + + Input array. + + axis : int, optional + + Axis along which the cumulative sum is computed. The default + (None) is to compute the cumsum over the flattened array. + + dtype : np.dtype, optional + + Type of the returned array and of the accumulator in which the + elements are summed. If `dtype` is not specified, it defaults + to the dtype of `a`, unless `a` has an integer dtype with a + precision less than that of the default platform integer. In + that case, the default platform integer is used. + + Returns + ------- + nancumsum : np.ndarray or pd.Series + + A new array that has the same size as a, and the same shape as a. + + See Also + -------- + numpy.cumsum : Cumulative sum across array propagating NaNs. + + """ + y = np.array(a, subok=True) + mask = np.isnan(a) + np.putmask(y, mask, 0.) + result = np.cumsum(y, axis=axis, dtype=dtype) + np.putmask(result, mask, np.nan) + return result + + +def aggregate_returns(returns, convert_to): + """ + Aggregates returns by week, month, or year. + + Parameters + ---------- + returns : pd.Series + Daily returns of the strategy, noncumulative. + - See full explanation in :func:`~empyrical.stats.cum_returns`. + convert_to : str + Can be 'weekly', 'monthly', or 'yearly'. + + Returns + ------- + pd.Series + Aggregated returns. + """ + + def cumulate_returns(x): + return cum_returns(x).iloc[-1] + + if convert_to == WEEKLY: + grouping = [lambda x: x.year, lambda x: x.isocalendar()[1]] + elif convert_to == MONTHLY: + grouping = [lambda x: x.year, lambda x: x.month] + elif convert_to == YEARLY: + grouping = [lambda x: x.year] + else: + raise ValueError( + 'convert_to must be {}, {} or {}'.format(WEEKLY, MONTHLY, YEARLY) + ) + + return returns.groupby(grouping).apply(cumulate_returns) + + +def max_drawdown(returns): + """ + Determines the maximum drawdown of a strategy. + + Parameters + ---------- + returns : pd.Series or np.ndarray + Daily returns of the strategy, noncumulative. + - See full explanation in :func:`~empyrical.stats.cum_returns`. + + Returns + ------- + float + Maximum drawdown. + + Note + ----- + See https://en.wikipedia.org/wiki/Drawdown_(economics) for more details. + """ + + if len(returns) < 1: + return np.nan + + cumulative = cum_returns(returns, starting_value=100) + max_return = np.fmax.accumulate(cumulative) + return nanmin((cumulative - max_return) / max_return) + + +def annual_return(returns, period=DAILY, annualization=None): + """Determines the mean annual growth rate of returns. + + Parameters + ---------- + returns : pd.Series or np.ndarray + Periodic returns of the strategy, noncumulative. + - See full explanation in :func:`~empyrical.stats.cum_returns`. + period : str, optional + Defines the periodicity of the 'returns' data for purposes of + annualizing. Value ignored if `annualization` parameter is specified. + Defaults are: + 'monthly':12 + 'weekly': 52 + 'daily': 252 + annualization : int, optional + Used to suppress default values available in `period` to convert + returns into annual returns. Value should be the annual frequency of + `returns`. + + Returns + ------- + float + Annual Return as CAGR (Compounded Annual Growth Rate). + + """ + + if len(returns) < 1: + return np.nan + + ann_factor = annualization_factor(period, annualization) + + num_years = float(len(returns)) / ann_factor + start_value = 100 + # Pass array to ensure index -1 looks up successfully. + end_value = cum_returns(np.asanyarray(returns), + starting_value=start_value)[-1] + cum_returns_final = (end_value - start_value) / start_value + annual_return = (1. + cum_returns_final) ** (1. / num_years) - 1 + + return annual_return + + +def annual_volatility(returns, period=DAILY, alpha=2.0, + annualization=None): + """ + Determines the annual volatility of a strategy. + + Parameters + ---------- + returns : pd.Series or np.ndarray + Periodic returns of the strategy, noncumulative. + - See full explanation in :func:`~empyrical.stats.cum_returns`. + period : str, optional + Defines the periodicity of the 'returns' data for purposes of + annualizing. Value ignored if `annualization` parameter is specified. + Defaults are: + 'monthly':12 + 'weekly': 52 + 'daily': 252 + alpha : float, optional + Scaling relation (Levy stability exponent). + annualization : int, optional + Used to suppress default values available in `period` to convert + returns into annual returns. Value should be the annual frequency of + `returns`. + + Returns + ------- + float + Annual volatility. + """ + + if len(returns) < 2: + return np.nan + + ann_factor = annualization_factor(period, annualization) + + volatility = nanstd(returns, ddof=1) * (ann_factor ** (1.0 / alpha)) + + return volatility + + +def calmar_ratio(returns, period=DAILY, annualization=None): + """ + Determines the Calmar ratio, or drawdown ratio, of a strategy. + + Parameters + ---------- + returns : pd.Series or np.ndarray + Daily returns of the strategy, noncumulative. + - See full explanation in :func:`~empyrical.stats.cum_returns`. + period : str, optional + Defines the periodicity of the 'returns' data for purposes of + annualizing. Value ignored if `annualization` parameter is specified. + Defaults are: + 'monthly':12 + 'weekly': 52 + 'daily': 252 + annualization : int, optional + Used to suppress default values available in `period` to convert + returns into annual returns. Value should be the annual frequency of + `returns`. + + + Returns + ------- + float + Calmar ratio (drawdown ratio) as float. Returns np.nan if there is no + calmar ratio. + + Note + ----- + See https://en.wikipedia.org/wiki/Calmar_ratio for more details. + """ + + max_dd = max_drawdown(returns=returns) + if max_dd < 0: + temp = annual_return( + returns=returns, + period=period, + annualization=annualization + ) / abs(max_dd) + else: + return np.nan + + if np.isinf(temp): + return np.nan + + return temp + + +def omega_ratio(returns, risk_free=0.0, required_return=0.0, + annualization=APPROX_BDAYS_PER_YEAR): + """Determines the Omega ratio of a strategy. + + Parameters + ---------- + returns : pd.Series or np.ndarray + Daily returns of the strategy, noncumulative. + - See full explanation in :func:`~empyrical.stats.cum_returns`. + risk_free : int, float + Constant risk-free return throughout the period + required_return : float, optional + Minimum acceptance return of the investor. Threshold over which to + consider positive vs negative returns. It will be converted to a + value appropriate for the period of the returns. E.g. An annual minimum + acceptable return of 100 will translate to a minimum acceptable + return of 0.018. + annualization : int, optional + Factor used to convert the required_return into a daily + value. Enter 1 if no time period conversion is necessary. + + Returns + ------- + float + Omega ratio. + + Note + ----- + See https://en.wikipedia.org/wiki/Omega_ratio for more details. + + """ + + if len(returns) < 2: + return np.nan + + if annualization == 1: + return_threshold = required_return + elif required_return <= -1: + return np.nan + else: + return_threshold = (1 + required_return) ** \ + (1. / annualization) - 1 + + returns_less_thresh = returns - risk_free - return_threshold + + numer = sum(returns_less_thresh[returns_less_thresh > 0.0]) + denom = -1.0 * sum(returns_less_thresh[returns_less_thresh < 0.0]) + + if denom > 0.0: + return numer / denom + else: + return np.nan + + +def sharpe_ratio(returns, risk_free=0, period=DAILY, annualization=None): + """ + Determines the Sharpe ratio of a strategy. + + Parameters + ---------- + returns : pd.Series or np.ndarray + Daily returns of the strategy, noncumulative. + - See full explanation in :func:`~empyrical.stats.cum_returns`. + risk_free : int, float + Constant risk-free return throughout the period. + period : str, optional + Defines the periodicity of the 'returns' data for purposes of + annualizing. Value ignored if `annualization` parameter is specified. + Defaults are: + 'monthly':12 + 'weekly': 52 + 'daily': 252 + annualization : int, optional + Used to suppress default values available in `period` to convert + returns into annual returns. Value should be the annual frequency of + `returns`. + + Returns + ------- + float + Sharpe ratio. + + np.nan + If insufficient length of returns or if if adjusted returns are 0. + + Note + ----- + See https://en.wikipedia.org/wiki/Sharpe_ratio for more details. + + """ + + if len(returns) < 2: + return np.nan + + ann_factor = annualization_factor(period, annualization) + + returns_risk_adj = np.asanyarray(_adjust_returns(returns, risk_free)) + returns_risk_adj = returns_risk_adj[~np.isnan(returns_risk_adj)] + + if np.std(returns_risk_adj, ddof=1) == 0: + return np.nan + + return np.mean(returns_risk_adj) / np.std(returns_risk_adj, ddof=1) * \ + np.sqrt(ann_factor) + + +def sortino_ratio(returns, required_return=0, period=DAILY, + annualization=None, _downside_risk=None): + """ + Determines the Sortino ratio of a strategy. + + Parameters + ---------- + returns : pd.Series or np.ndarray or pd.DataFrame + Daily returns of the strategy, noncumulative. + - See full explanation in :func:`~empyrical.stats.cum_returns`. + required_return: float / series + minimum acceptable return + period : str, optional + Defines the periodicity of the 'returns' data for purposes of + annualizing. Value ignored if `annualization` parameter is specified. + Defaults are: + 'monthly':12 + 'weekly': 52 + 'daily': 252 + annualization : int, optional + Used to suppress default values available in `period` to convert + returns into annual returns. Value should be the annual frequency of + `returns`. + _downside_risk : float, optional + The downside risk of the given inputs, if known. Will be calculated if + not provided. + + Returns + ------- + float, pd.Series + + depends on input type + series ==> float + DataFrame ==> pd.Series + + Annualized Sortino ratio. + + """ + + if len(returns) < 2: + return np.nan + + ann_factor = annualization_factor(period, annualization) + + adj_returns = _adjust_returns(returns, required_return) + mu = nanmean(adj_returns, axis=0) + dsr = (_downside_risk if _downside_risk is not None + else downside_risk(returns, required_return)) + sortino = mu / dsr + return sortino * ann_factor + + +def downside_risk(returns, required_return=0, period=DAILY, + annualization=None): + """ + Determines the downside deviation below a threshold + + Parameters + ---------- + returns : pd.Series or np.ndarray or pd.DataFrame + Daily returns of the strategy, noncumulative. + - See full explanation in :func:`~empyrical.stats.cum_returns`. + required_return: float / series + minimum acceptable return + period : str, optional + Defines the periodicity of the 'returns' data for purposes of + annualizing. Value ignored if `annualization` parameter is specified. + Defaults are: + 'monthly':12 + 'weekly': 52 + 'daily': 252 + annualization : int, optional + Used to suppress default values available in `period` to convert + returns into annual returns. Value should be the annual frequency of + `returns`. + + Returns + ------- + float, pd.Series + depends on input type + series ==> float + DataFrame ==> pd.Series + + Annualized downside deviation + + """ + + if len(returns) < 1: + return np.nan + + ann_factor = annualization_factor(period, annualization) + + downside_diff = _adjust_returns(returns, required_return).copy() + mask = downside_diff > 0 + downside_diff[mask] = 0.0 + squares = np.square(downside_diff) + mean_squares = nanmean(squares, axis=0) + dside_risk = np.sqrt(mean_squares) * np.sqrt(ann_factor) + + if len(returns.shape) == 2 and isinstance(returns, pd.DataFrame): + dside_risk = pd.Series(dside_risk, index=returns.columns) + return dside_risk + + +def information_ratio(returns, factor_returns): + """ + Determines the Information ratio of a strategy. + + Parameters + ---------- + returns : pd.Series or np.ndarray + Daily returns of the strategy, noncumulative. + - See full explanation in :func:`~empyrical.stats.cum_returns`. + factor_returns: float / series + Benchmark return to compare returns against. + + Returns + ------- + float + The information ratio. + + Note + ----- + See https://en.wikipedia.org/wiki/information_ratio for more details. + + """ + if len(returns) < 2: + return np.nan + + active_return = _adjust_returns(returns, factor_returns) + tracking_error = nanstd(active_return, ddof=1) + if np.isnan(tracking_error): + return 0.0 + if tracking_error == 0: + return np.nan + return nanmean(active_return) / tracking_error + + +def _aligned_series(*many_series): + """ + Return a new list of series containing the data in the input series, but + with their indices aligned. NaNs will be filled in for missing values. + + Parameters + ---------- + many_series : list[pd.Series] + + Returns + ------- + aligned_series : list[pd.Series] + + A new list of series containing the data in the input series, but + with their indices aligned. NaNs will be filled in for missing values. + + """ + return [series + for col, series in iteritems(pd.concat(many_series, axis=1))] + + +def alpha_beta(returns, factor_returns, risk_free=0.0, period=DAILY, + annualization=None): + """Calculates annualized alpha and beta. + + Parameters + ---------- + returns : pd.Series + Daily returns of the strategy, noncumulative. + - See full explanation in :func:`~empyrical.stats.cum_returns`. + factor_returns : pd.Series + Daily noncumulative returns of the factor to which beta is + computed. Usually a benchmark such as the market. + - This is in the same style as returns. + risk_free : int, float, optional + Constant risk-free return throughout the period. For example, the + interest rate on a three month us treasury bill. + period : str, optional + Defines the periodicity of the 'returns' data for purposes of + annualizing. Value ignored if `annualization` parameter is specified. + Defaults are: + 'monthly':12 + 'weekly': 52 + 'daily': 252 + annualization : int, optional + Used to suppress default values available in `period` to convert + returns into annual returns. Value should be the annual frequency of + `returns`. + + Returns + ------- + float + Alpha. + float + Beta. + + """ + if len(returns) < 2 or len(factor_returns) < 2: + return np.nan, np.nan + + return alpha_beta_aligned(*_aligned_series(returns, factor_returns), + risk_free=risk_free, period=period, + annualization=annualization) + + +def alpha_beta_aligned(returns, factor_returns, risk_free=0.0, period=DAILY, + annualization=None): + """Calculates annualized alpha and beta. + + If they are pd.Series, expects returns and factor_returns have already + been aligned on their labels. If np.ndarray, these arguments should have + the same shape. + + Parameters + ---------- + returns : pd.Series or np.ndarray + Daily returns of the strategy, noncumulative. + - See full explanation in :func:`~empyrical.stats.cum_returns`. + factor_returns : pd.Series or np.ndarray + Daily noncumulative returns of the factor to which beta is + computed. Usually a benchmark such as the market. + - This is in the same style as returns. + risk_free : int, float, optional + Constant risk-free return throughout the period. For example, the + interest rate on a three month us treasury bill. + period : str, optional + Defines the periodicity of the 'returns' data for purposes of + annualizing. Value ignored if `annualization` parameter is specified. + Defaults are: + 'monthly':12 + 'weekly': 52 + 'daily': 252 + annualization : int, optional + Used to suppress default values available in `period` to convert + returns into annual returns. Value should be the annual frequency of + `returns`. + + Returns + ------- + float + Alpha. + float + Beta. + + """ + b = beta_aligned(returns, factor_returns, risk_free) + a = alpha_aligned(returns, factor_returns, risk_free, period, + annualization, _beta=b) + return a, b + + +def alpha(returns, factor_returns, risk_free=0.0, period=DAILY, + annualization=None, _beta=None): + """Calculates annualized alpha. + + Parameters + ---------- + returns : pd.Series + Daily returns of the strategy, noncumulative. + - See full explanation in :func:`~empyrical.stats.cum_returns`. + factor_returns : pd.Series + Daily noncumulative returns of the factor to which beta is + computed. Usually a benchmark such as the market. + - This is in the same style as returns. + risk_free : int, float, optional + Constant risk-free return throughout the period. For example, the + interest rate on a three month us treasury bill. + period : str, optional + Defines the periodicity of the 'returns' data for purposes of + annualizing. Value ignored if `annualization` parameter is specified. + Defaults are: + 'monthly':12 + 'weekly': 52 + 'daily': 252 + annualization : int, optional + Used to suppress default values available in `period` to convert + returns into annual returns. Value should be the annual frequency of + `returns`. + - See full explanation in :func:`~empyrical.stats.annual_return`. + _beta : float, optional + The beta for the given inputs, if already known. Will be calculated + internally if not provided. + + Returns + ------- + float + Alpha. + """ + if len(returns) < 2 or len(factor_returns) < 2: + return np.nan + + return alpha_aligned(*_aligned_series(returns, factor_returns), + risk_free=risk_free, period=period, + annualization=annualization, _beta=_beta) + + +def alpha_aligned(returns, factor_returns, risk_free=0.0, period=DAILY, + annualization=None, _beta=None): + """Calculates annualized alpha. + + If they are pd.Series, expects returns and factor_returns have already + been aligned on their labels. If np.ndarray, these arguments should have + the same shape. + + Parameters + ---------- + returns : pd.Series or np.ndarray + Daily returns of the strategy, noncumulative. + - See full explanation in :func:`~empyrical.stats.cum_returns`. + factor_returns : pd.Series or np.ndarray + Daily noncumulative returns of the factor to which beta is + computed. Usually a benchmark such as the market. + - This is in the same style as returns. + risk_free : int, float, optional + Constant risk-free return throughout the period. For example, the + interest rate on a three month us treasury bill. + period : str, optional + Defines the periodicity of the 'returns' data for purposes of + annualizing. Value ignored if `annualization` parameter is specified. + Defaults are: + 'monthly':12 + 'weekly': 52 + 'daily': 252 + annualization : int, optional + Used to suppress default values available in `period` to convert + returns into annual returns. Value should be the annual frequency of + `returns`. + - See full explanation in :func:`~empyrical.stats.annual_return`. + _beta : float, optional + The beta for the given inputs, if already known. Will be calculated + internally if not provided. + + Returns + ------- + float + Alpha. + """ + if len(returns) < 2: + return np.nan + + ann_factor = annualization_factor(period, annualization) + + if _beta is None: + _beta = beta_aligned(returns, factor_returns, risk_free) + + adj_returns = _adjust_returns(returns, risk_free) + adj_factor_returns = _adjust_returns(factor_returns, risk_free) + alpha_series = adj_returns - (_beta * adj_factor_returns) + + return nanmean(alpha_series) * ann_factor + + +def beta(returns, factor_returns, risk_free=0.0): + """Calculates beta. + + Parameters + ---------- + returns : pd.Series + Daily returns of the strategy, noncumulative. + - See full explanation in :func:`~empyrical.stats.cum_returns`. + factor_returns : pd.Series + Daily noncumulative returns of the factor to which beta is + computed. Usually a benchmark such as the market. + - This is in the same style as returns. + risk_free : int, float, optional + Constant risk-free return throughout the period. For example, the + interest rate on a three month us treasury bill. + + Returns + ------- + float + Beta. + """ + if len(returns) < 2 or len(factor_returns) < 2: + return np.nan + + return beta_aligned(*_aligned_series(returns, factor_returns), + risk_free=risk_free) + + +def beta_aligned(returns, factor_returns, risk_free=0.0): + """Calculates beta. + + If they are pd.Series, expects returns and factor_returns have already + been aligned on their labels. If np.ndarray, these arguments should have + the same shape. + + Parameters + ---------- + returns : pd.Series or np.ndarray + Daily returns of the strategy, noncumulative. + - See full explanation in :func:`~empyrical.stats.cum_returns`. + factor_returns : pd.Series or np.ndarray + Daily noncumulative returns of the factor to which beta is + computed. Usually a benchmark such as the market. + - This is in the same style as returns. + risk_free : int, float, optional + Constant risk-free return throughout the period. For example, the + interest rate on a three month us treasury bill. + + Returns + ------- + float + Beta. + """ + + if len(returns) < 2 or len(factor_returns) < 2: + return np.nan + # Filter out dates with np.nan as a return value + joint = np.vstack([_adjust_returns(returns, risk_free), + factor_returns]) + joint = joint[:, ~np.isnan(joint).any(axis=0)] + if joint.shape[1] < 2: + return np.nan + + cov = np.cov(joint, ddof=0) + + if np.absolute(cov[1, 1]) < 1.0e-30: + return np.nan + + return cov[0, 1] / cov[1, 1] + + +def stability_of_timeseries(returns): + """Determines R-squared of a linear fit to the cumulative + log returns. Computes an ordinary least squares linear fit, + and returns R-squared. + + Parameters + ---------- + returns : pd.Series or np.ndarray + Daily returns of the strategy, noncumulative. + - See full explanation in :func:`~empyrical.stats.cum_returns`. + + Returns + ------- + float + R-squared. + + """ + if len(returns) < 2: + return np.nan + + returns = np.asanyarray(returns) + returns = returns[~np.isnan(returns)] + + cum_log_returns = np.log1p(returns).cumsum() + rhat = stats.linregress(np.arange(len(cum_log_returns)), + cum_log_returns)[2] + + return rhat ** 2 + + +def tail_ratio(returns): + """Determines the ratio between the right (95%) and left tail (5%). + + For example, a ratio of 0.25 means that losses are four times + as bad as profits. + + Parameters + ---------- + returns : pd.Series or np.ndarray + Daily returns of the strategy, noncumulative. + - See full explanation in :func:`~empyrical.stats.cum_returns`. + + Returns + ------- + float + tail ratio + + """ + + if len(returns) < 1: + return np.nan + + returns = np.asanyarray(returns) + # Be tolerant of nan's + returns = returns[~np.isnan(returns)] + if len(returns) < 1: + return np.nan + + return np.abs(np.percentile(returns, 95)) / \ + np.abs(np.percentile(returns, 5)) + + +def cagr(returns, period=DAILY, annualization=None): + """ + Compute compound annual growth rate. + + Parameters + ---------- + returns : pd.Series or np.ndarray + Daily returns of the strategy, noncumulative. + - See full explanation in :func:`~empyrical.stats.cum_returns`. + period : str, optional + Defines the periodicity of the 'returns' data for purposes of + annualizing. Value ignored if `annualization` parameter is specified. + Defaults are: + 'monthly':12 + 'weekly': 52 + 'daily': 252 + annualization : int, optional + Used to suppress default values available in `period` to convert + returns into annual returns. Value should be the annual frequency of + `returns`. + - See full explanation in :func:`~empyrical.stats.annual_return`. + + Returns + ------- + float, np.nan + The CAGR value. + + """ + if len(returns) < 1: + return np.nan + + ann_factor = annualization_factor(period, annualization) + no_years = len(returns) / float(ann_factor) + # Pass array to ensure index -1 looks up successfully. + ending_value = cum_returns(np.asanyarray(returns), starting_value=1)[-1] + + return ending_value ** (1. / no_years) - 1 + + +SIMPLE_STAT_FUNCS = [ + cum_returns_final, + annual_return, + annual_volatility, + sharpe_ratio, + calmar_ratio, + stability_of_timeseries, + max_drawdown, + omega_ratio, + sortino_ratio, + stats.skew, + stats.kurtosis, + tail_ratio, + cagr +] + +FACTOR_STAT_FUNCS = [ + information_ratio, + alpha, + beta, +] diff --git a/catalyst/support/issue_126.py b/catalyst/support/issue_126.py new file mode 100644 index 00000000..3988c1f1 --- /dev/null +++ b/catalyst/support/issue_126.py @@ -0,0 +1,34 @@ +from catalyst import run_algorithm +from catalyst.api import order, record, symbol +import pandas as pd + + +def initialize(context): + context.asset = symbol('btc_usdt') + + +def handle_data(context, data): + order(context.asset, 1) + + price = data.current(context.asset, 'price') + record(btc=price) + pass + + +def analyze(context, perf): + pass + + +if __name__ == '__main__': + run_algorithm( + capital_base=1000, + data_frequency='daily', + initialize=initialize, + handle_data=handle_data, + exchange_name='poloniex', + algo_namespace='buy_btc_polo_jh', + base_currency='usd', + analyze=analyze, + start=pd.to_datetime('2017-01-01', utc=True), + end=pd.to_datetime('2017-12-25', utc=True), + )