Source code for fynance.features.metrics

#!/usr/bin/env python3
# coding: utf-8
# @Author: ArthurBernard
# @Email: arthur.bernard.92@gmail.com
# @Date: 2018-12-14 19:11:40
# @Last modified by: ArthurBernard
# @Last modified time: 2020-01-24 15:57:17

""" Performance and risk metrics for financial analysis.

Compute risk-adjusted returns, drawdown statistics and other summary
indicators commonly used to evaluate strategies and portfolios. All
functions accept a 1-D or 2-D array of prices/returns and return the
metric along the time axis.

Main entry points
-----------------
- :func:`annual_return`, :func:`annual_volatility` — annualized
  return and volatility from a price series.
- :func:`sharpe`, :func:`calmar`, :func:`diversified_ratio` —
  risk-adjusted performance ratios.
- :func:`mdd`, :func:`drawdown` — maximum drawdown and drawdown path.
- :func:`z_score`, :func:`accuracy` — statistical helpers.

"""

from __future__ import annotations

# Built-in packages
from warnings import warn

# External packages
import numpy as np
from numpy.typing import NDArray

from fynance._exceptions import ArraySizeError

# Internal packages
from fynance._wrappers import WrapperArray
from fynance.features.metrics_cy import *
from fynance.features.momentums import _ema, _emstd, _sma, _smstd, _wma, _wmstd

# TODO:
# - Append performance
# - Append rolling performance
# - verify and fix error to perf_strat, perf_returns, perf_index


__all__ = [
    'accuracy', 'annual_return', 'annual_volatility', 'calmar',
    'directional_accuracy', 'diversified_ratio', 'drawdown', 'mad', 'mdd',
    'roll_annual_return', 'roll_annual_volatility', 'roll_calmar',
    'roll_drawdown', 'roll_mad', 'roll_mdd', 'roll_sharpe', 'roll_z_score',
    'sharpe', 'sortino', 'perf_index', 'perf_returns', 'z_score',
]

_handler_ma = {'s': _sma, 'w': _wma, 'e': _ema}
_handler_mstd = {'s': _smstd, 'w': _wmstd, 'e': _emstd}

# =========================================================================== #
#                                   Metrics                                   #
# =========================================================================== #


[docs] @WrapperArray('axis') def accuracy(y_true: NDArray, y_pred: NDArray, sign: bool = True, axis: int = 0) -> float: r""" Compute the accuracy of prediction. Notes ----- .. math:: accuracy = \frac{right}{right + wrong} Parameters ---------- y_true : np.ndarray[ndim=1 or 2, dtype] Vector of true series. y_pred : np.ndarray[ndim=1 or 2, dtype] Vector of predicted series. sign : bool, optional - If True then check sign accuracy (default). - Else check exact accuracy. axis : {0, 1}, optional Axis along wich the computation is done. Default is 0. Returns ------- float or np.ndarray[ndim=1, float] Accuracy of prediction as float between 0 and 1. Examples -------- >>> y_true = np.array([1., .5, -.5, .8, -.2]) >>> y_pred = np.array([.5, .2, -.5, .1, .0]) >>> accuracy(y_true, y_pred) 0.8 >>> accuracy(y_true, y_pred, sign=False) 0.2 See Also -------- mdd, calmar, sharpe, drawdown """ if sign: y_true = np.sign(y_true) y_pred = np.sign(y_pred) return np.sum(y_true == y_pred, axis=axis) / y_true.shape[axis]
[docs] @WrapperArray('dtype', 'axis', 'ddof', min_size=2) def annual_return(X: NDArray, period: int = 252, axis: int = 0, dtype=None, ddof: int = 0) -> NDArray: r""" Compute compouned annual returns of each `X`' series. The annualised return [1]_ is the process of converting returns on a whole period to returns per year. Notes ----- Let T the number of timeframes in `X`' series, the annual compouned returns is computed such that: .. math:: annualReturn = \frac{X_T}{X_1}^{\frac{period}{T}} - 1 Parameters ---------- X : np.ndarray[dtype, ndim=1 or 2] Time-series of price, performance or index. period : int, optional Number of period per year, default is 252 (trading days per year). axis : {0, 1}, optional Axis along wich the computation is done. Default is 0. dtype : np.dtype, optional The type of the output array. If `dtype` is not given, infer the data type from `X` input. ddof : int, optional Means Delta Degrees of Freedom, the divisor used in calculations is ``T - ddof``, where ``T`` represents the number of elements in time axis. Default is 0. Returns ------- dtype or np.ndarray[dtype, ndim=1] Values of compouned annual returns of each series. References ---------- .. [1] https://en.wikipedia.org/wiki/Rate_of_return#Annualisation Examples -------- Assume series of monthly prices: >>> X = np.array([100, 110, 80, 120, 160, 108]).astype(np.float64) >>> print(round(annual_return(X, period=12), 4)) 0.1664 >>> X = np.array([[100, 110], [80, 120], [160, 108]]).astype(np.float64) >>> annual_return(X, period=12, ddof=1) array([15.777216 , -0.10425081]) See Also -------- mdd, drawdown, sharpe, annual_volatility """ if ddof >= X.shape[0]: raise ValueError("degree of freedom {} is greater than size {} of X " "in axis {}".format(ddof, X.shape[axis], axis)) return _annual_return(X, period, ddof)
def _annual_return(X, period, ddof): if (X[0] == 0).any(): raise ValueError('initial value X[0] cannot be null.') ret = X[-1] / X[0] T = X.shape[0] if (ret < 0).any(): raise ValueError('initial value X[0] and final value X[-1] must ' 'be of the same sign.') sign = np.sign(X[0]) power = period / (T - ddof) return sign * np.float_power(ret, power, dtype=np.float64) - 1.
[docs] @WrapperArray('dtype', 'axis', 'null', 'ddof', min_size=2) def annual_volatility(X: NDArray, period: int = 252, log: bool = True, axis: int = 0, dtype=None, ddof: int = 0) -> NDArray: r""" Compute the annualized volatility of each `X`' series. In finance, volatility is the degree of variation of a trading price series over time as measured by the standard deviation of logarithmic returns [2]_. Notes ----- Let :math:`Var` the variance function of a random variable: .. math:: annualVolatility = \sqrt{period \times Var(R_{1:T})} Where, :math:`R_1 = 0` and :math:`R_{2:T} = \begin{cases}ln(\frac{X_{2:T}} {X_{1:T-1}}) \text{, if log=True} \\ \frac{X_{2:T}}{X_{1:T-1}} - 1 \text{, otherwise} \\ \end{cases}` Parameters ---------- X : np.ndarray[dtype, ndim=1 or 2] Time-series of price, performance or index. period : int, optional Number of period per year, default is 252 (trading days per year). log : bool, optional - If True then logarithmic returns are computed. - Else then returns in percentage are computed. axis : {0, 1}, optional Axis along wich the computation is done. Default is 0. dtype : np.dtype, optional The type of the output array. If `dtype` is not given, infer the data type from `X` input. ddof : int, optional Means Delta Degrees of Freedom, the divisor used in calculations is ``T - ddof``, where ``T`` represents the number of elements in time axis. Default is 0. Returns ------- dtype or np.ndarray([dtype, ndim=1]) Values of annualized volatility for each series. References ---------- .. [2] https://en.wikipedia.org/wiki/Volatility_(finance) Examples -------- Assume series of monthly prices: >>> X = np.array([100, 110, 105, 110, 120, 108]).astype(np.float64) >>> annual_volatility(X, period=12, log=True, ddof=1) 0.2731896268610321 >>> annual_volatility(X.reshape([6, 1]), period=12, log=False) array([0.24961719]) See Also -------- mdd, drawdown, sharpe, annual_return """ return _annual_volatility(X, period, log, axis, ddof)
def _compute_returns(X, log): R = np.zeros(X.shape) if log: R[1:] = np.log(X[1:] / X[:-1]) else: R[1:] = X[1:] / X[:-1] - 1. return R def _annual_volatility(X, period, log, axis, ddof): return np.sqrt(period) * np.std(_compute_returns(X, log), axis=axis, ddof=ddof) def _annual_downside_volatility(X, period, log, axis, ddof): R = _compute_returns(X, log) return np.sqrt(period) * np.std(np.where(R < 0, R, 0.), axis=axis, ddof=ddof)
[docs] @WrapperArray('dtype', 'axis', 'ddof', min_size=2) def calmar(X: NDArray, period: int = 252, axis: int = 0, dtype=None, ddof: int = 0) -> NDArray: r""" Compute the Calmar Ratio for each `X`' series. Notes ----- Calmar ratio [3]_ is the compouned annual return (:func:`~fynance.features.metrics.annual_return`) over the maximum drawdown (:func:`~fynance.features.metrics.mdd`). Let :math:`T` the number of time observations, DD the vector of drawdown: .. math:: calmarRatio = \frac{annualReturn}{MDD} With, :math:`annualReturn = \frac{X_T}{X_1}^{\frac{period}{T}} - 1` and :math:`MDD = max(DD_{1:T})`. Where, :math:`DD_t = 1 - \frac{X_t}{max(X_{1:t})}`, :math:`\forall t \in [1:T]`. Parameters ---------- X : np.ndarray[dtype, ndim=1 or 2] Time-series price, performance or index. period : int, optional Number of period per year, default is 252 (trading days per year). axis : {0, 1}, optional Axis along wich the computation is done. Default is 0. dtype : np.dtype, optional The type of the output array. If `dtype` is not given, infer the data type from `X` input. ddof : int, optional Means Delta Degrees of Freedom, the divisor used in calculations is ``T - ddof``, where ``T`` represents the number of elements in time axis. Default is 0. Returns ------- dtype or np.ndarray([dtype, ndim=1]) Values of Calmar ratio for each series. References ---------- .. [3] https://en.wikipedia.org/wiki/Calmar_ratio Examples -------- Assume a series of monthly prices: >>> X = np.array([70, 100, 80, 120, 160, 105, 80]).astype(np.float64) >>> calmar(X, period=12, ddof=1) array(0.6122449) >>> calmar(X.reshape([7, 1]), period=12) array([0.51446018]) See Also -------- mdd, drawdown, sharpe, roll_calmar """ ret = _annual_return(X, period, ddof) dd = _drawdown(X, False) mdd = np.max(dd, axis=axis) calmar = np.zeros(ret.shape) slice_bool = (mdd != 0) calmar[slice_bool] = ret[slice_bool] / mdd[slice_bool] return calmar
[docs] @WrapperArray('axis') def diversified_ratio(X: NDArray, W: NDArray | None = None, std_method: str = 'std', axis: int = 0) -> float: """ Compute diversification ratio of a portfolio. Notes ----- Diversification ratio, denoted D, is defined as the ratio of the portfolio's weighted average volatility to its overll volatility, developed by Choueifaty and Coignard [4]_. .. math:: D(P) = \\frac{P' \\Sigma}{\\sqrt{P'VP}} With :math:`\\Sigma` vector of asset volatilities, :math:`P` vector of weights of asset of portfolio, and :math:`V` matrix of variance-covariance of these assets. Parameters ---------- X : np.ndarray[ndim=2, dtype=np.float64] of shape (T, N) Portfolio matrix of N assets and T time periods, each column correspond to one series of prices. W : np.array[ndim=1 or 2, dtype=np.float64] of size N, optional Vector of weights, default is None it means it will equaly weighted. std_method : str, optional /!\\ Not yet implemented /!\ Method to compute variance vector and covariance matrix. axis : {0, 1}, optional Axis along wich the computation is done. Default is 0. Returns ------- np.float64 Value of diversification ratio of the portfolio. References ---------- .. [4] `Choueifaty, Y., and Coignard, Y., 2008, Toward Maximum \ Diversification. <https://www.tobam.fr/wp-content/uploads/2014/12/\ TOBAM-JoPM-Maximum-Div-2008.pdf>`_ """ # TODO : check efficiency # append examples T, N = X.shape if W is None: W = np.ones([N, 1]) / N else: W = W.reshape([N, 1]) sigma = np.std(X, axis=0).reshape([N, 1]) V = np.cov(X, rowvar=False, bias=True).reshape([N, N]) return (W.T @ sigma) / np.sqrt(W.T @ V @ W)
[docs] @WrapperArray('dtype', 'axis') def drawdown(X: NDArray, raw: bool = False, axis: int = 0, dtype=None) -> NDArray: r""" Measures the drawdown of each `X`' series. Function to compute measure of the decline from a historical peak in some variable [5]_ (typically the cumulative profit or total open equity of a financial trading strategy). Notes ----- Let DD the drawdown vector, :math:`\forall t \in [1:T]`: .. math:: DD_t = \begin{cases}max(X_{1:t}) - X_t \text{, if raw=True} \\ 1 - \frac{X_t}{max(X_{1:t})} \text{, otherwise} \\ \end{cases} Parameters ---------- X : np.ndarray[dtype, ndim=1 or 2] Time-series of prices, performances or index. Must be positive values. raw : bool, optional - If True then compute the raw drawdown. - Else (default) compute the drawdown in percentage. axis : {0, 1}, optional Axis along wich the computation is done. Default is 0. dtype : np.dtype, optional The type of the output array. If `dtype` is not given, infer the data type from `X` input. Returns ------- np.ndarray[dtype, ndim=1 or 2] Series of drawdown for each series. References ---------- .. [5] https://en.wikipedia.org/wiki/Drawdown_(economics) Examples -------- >>> X = np.array([70, 100, 80, 120, 160, 80]).astype(np.float64) >>> drawdown(X) array([0. , 0. , 0.2, 0. , 0. , 0.5]) >>> drawdown(X.reshape([6, 1])).T array([[0. , 0. , 0.2, 0. , 0. , 0.5]]) >>> drawdown(X, raw=True) array([ 0., 0., 20., 0., 0., 80.]) See Also -------- mdd, calmar, sharpe, roll_mdd """ return _drawdown(X, raw)
def _drawdown(X, raw): if (X[0] == 0).any() and not raw: warn( 'Cannot compute drawdown in percentage without initial values ' 'X[0] strictly positive.', category=UserWarning, stacklevel=2, ) raw = True if len(X.shape) == 2: return np.asarray(drawdown_cy_2d(X, int(raw))) return np.asarray(drawdown_cy_1d(X, int(raw)))
[docs] @WrapperArray('dtype') def mad(X: NDArray, axis: int = 0, dtype=None) -> NDArray: """ Compute the Mean Absolute Deviation of each `X`' series. Compute the mean of the absolute value of the distance to the mean [6]_. Parameters ---------- X : np.ndarray[np.dtype, ndim=1 or 2] Time-series of prices, performances or index. axis : {0, 1}, optional Axis along wich the computation is done. Default is 0. dtype : np.dtype, optional The type of the output array. If `dtype` is not given, infer the data type from `X` input. Returns ------- dtype or np.ndarray[dtype, ndim=1] Values of mean absolute deviation of each series. References ---------- .. [6] https://en.wikipedia.org/wiki/Average_absolute_deviation Examples -------- >>> X = np.array([70., 100., 90., 110., 150., 80.]) >>> mad(X) 20.0 See Also -------- roll_mad """ # TODO : make cython function or not ? return np.mean(np.abs(X.T - np.mean(X, axis=axis)).T, axis=axis)
[docs] @WrapperArray('dtype', 'axis') def mdd(X: NDArray, raw: bool = False, axis: int = 0, dtype=None) -> NDArray: r""" Compute the maximum drawdown for each `X`' series. Maximum peak-to-trough decline observed over the full series. A standard tail-risk indicator: it captures the worst loss an investor would have endured, regardless of horizon. Reported in relative terms by default (fraction of peak); use ``raw=True`` for an absolute decline. For the full drawdown path use :func:`drawdown`; combined with annual return, it gives the Calmar ratio (:func:`calmar`). Drawdown (:func:~`fynance.features.metrics.drawdown`) is the measure of the decline from a historical peak in some variable [5]_ (typically the cumulative profit or total open equity of a financial trading strategy). Notes ----- Let DD the drawdown vector: .. math:: MDD = max(DD_{1:T}) Where, :math:`DD_t = \begin{cases}max(X_{1:t}) - X_t \text{, if raw=True} \\ 1 - \frac{X_t}{max(X_{1:t})} \text{, otherwise} \\ \end{cases}`, :math:`\forall t \in [1:T]`. Parameters ---------- X : np.ndarray[np.dtype, ndim=1 or 2] Time-series of prices, performances or index. raw : bool, optional - If True then compute the raw drawdown. - Else (default) compute the drawdown in percentage. axis : {0, 1}, optional Axis along wich the computation is done. Default is 0. dtype : np.dtype, optional The type of the output array. If `dtype` is not given, infer the data type from `X` input. Returns ------- dtype or np.ndarray[dtype, ndim=1] Value of Maximum DrawDown for each series. References ---------- .. [5] https://en.wikipedia.org/wiki/Drawdown_(economics) Examples -------- >>> X = np.array([70, 100, 80, 120, 160, 80]).astype(np.float64) >>> mdd(X) 0.5 >>> mdd(X.reshape([6, 1])) array([0.5]) See Also -------- drawdown, calmar, sharpe, roll_mdd """ return _drawdown(X, raw).max(axis=axis)
[docs] @WrapperArray('dtype', 'axis') def perf_index(X: NDArray, base: float = 100., axis: int = 0, dtype=None) -> NDArray: """ Compute performance of prices or index values along time axis. Parameters ---------- X : np.ndarray[dtype, ndim=1 or 2] Time-series of prices or index values. base : float, optional Initial value for measure the performance, default is 100. axis : {0, 1}, optional Axis along wich the computation is done. Default is 0. dtype : np.dtype, optional The type of the output array. If `dtype` is not given, infer the data type from `X` input. Returns ------- np.ndarray[dtype, ndim=1 or 2] Performances along time axis. See Also -------- perf_returns, perf_strat Examples -------- >>> X = np.array([10., 12., 15., 14., 16., 18., 16.]) >>> perf_index(X, base=100.) array([100., 120., 150., 140., 160., 180., 160.]) """ return base * X / X[0]
[docs] @WrapperArray('dtype', 'axis') def perf_returns(R: NDArray, kind: str = 'raw', base: float = 100., axis: int = 0, dtype=None) -> NDArray: """ Compute performance of returns along time axis. Parameters ---------- R : np.ndarray[dtype, ndim=1 or 2] Time-series of returns. kind : {'raw', 'log', 'pct'} - If `'raw'` (default), then considers returns as following :math:`R_t = X_t - X_{t-1}`. - If `'log'`, then considers returns as following :math:`R_t = log(\\frac{X_t}{X_{t-1}})`. - If `'pct'`, then considers returns as following :math:`R_t = \\frac{X_t - X_{t-1}}{X_{t-1}}`. base : float, optional Initial value for measure the performance, default is 100. axis : {0, 1}, optional Axis along wich the computation is done. Default is 0. dtype : np.dtype, optional The type of the output array. If `dtype` is not given, infer the data type from `X` input. Returns ------- np.ndarray[dtype, ndim=1 or 2] Performances along time axis. See Also -------- perf_index, perf_strat Examples -------- >>> R = np.array([0., 20., 30., -10., 20., 20., -20.]) >>> perf_returns(R, base=100.) array([100., 120., 150., 140., 160., 180., 160.]) """ if kind.lower() == 'raw': X = base + np.cumsum(R, axis=axis) elif kind.lower() == 'log': X = base * np.cumprod(np.exp(R), axis=axis) elif kind.lower() == 'pct': X = base * np.cumprod(R + 1., axis=axis) else: raise ValueError("unkwnown kind {} of returns, only {'raw', 'log'," # noqa: F524 "'pct'} are supported".format(kind)) return perf_index(X, base=base, axis=axis, dtype=dtype)
[docs] @WrapperArray('dtype', 'axis') def perf_strat(X: NDArray, S: NDArray | None = None, base: float = 100., axis: int = 0, dtype=None, reinvest: bool = False) -> NDArray: """ Compute the performance of strategies for each `X`' series. With respect to this underlying and signal series along time axis. Parameters ---------- X : np.ndarray[dtype, ndim=1 or 2] Time-series of prices or index values. S : np.ndarray[dtype, ndim=1 or 2] Time-series of signals, if `None` considering a long only position. ``S`` array must have the same shape than ``X``. Default is None. base : float, optional Initial value for measure the performance, default is 100. reinvest : bool, optional - If True, then reinvest profit to compute the performance. - Otherwise (default), compute the performance without reinvesting. axis : {0, 1}, optional Axis along wich the computation is done. Default is 0. dtype : np.dtype, optional The type of the output array. If `dtype` is not given, infer the data type from `X` input. Returns ------- np.ndarray[dtype, ndim=1 or 2] Performances along time axis. See Also -------- perf_returns, perf_index Examples -------- >>> X = np.array([10., 12., 15., 14., 16., 18., 16.]) >>> S = np.array([1., 1., 1., 0., 1., 1., -1.]) >>> perf_strat(X, S, base=100.) array([100., 120., 150., 150., 170., 190., 210.]) >>> perf_strat(X, S, base=100., reinvest=True) array([100. , 120. , 150. , 150. , 171.42857143, 192.85714286, 214.28571429]) """ if S is None: S = np.ones(X.shape) elif S.ndim < X.ndim and S.shape[0] == X.shape[0]: S = S.reshape(S.shape + (1,) * (X.ndim - S.ndim)) elif S.shape == X.shape: pass else: raise ValueError('S and X could not be broadcast, must have the same ' 'shape or axis {} of same size and S.ndim < X.ndim ' '(but S:{} and X:{})'.format(axis, S.shape, X.shape)) R = np.zeros(X.shape) X = base * X / X[0] if not reinvest: kind = 'raw' R[1:] = X[1:] - X[:-1] elif reinvest: kind = 'pct' R[1:] = X[1:] / X[:-1] - 1 return perf_returns(R * S, base=base, kind=kind, axis=axis, dtype=dtype)
@WrapperArray('dtype', 'axis') def returns_strat(X: NDArray, S: NDArray | None = None, kind: str = 'pct', base: float = 100., axis: int = 0, dtype=None) -> NDArray: r""" Compute the returns of strategies for each `X`' series. With respect to this underlying and signal series along time axis. Parameters ---------- X : np.ndarray[dtype, ndim=1 or 2] Time-series of prices or index values. S : np.ndarray[dtype, ndim=1 or 2] Time-series of signals, if `None` considering a long only position. ``S`` array must have the same shape than ``X``. Default is None. kind : {'raw', 'pct'} - If `'raw'`, then considers returns as following :math:`R_t = X_t - X_{t-1}`. - If `'pct'` (default), then considers returns as following :math:`R_t = \frac{X_t - X_{t-1}}{X_{t-1}}`. base : float, optional Initial value for measure the returns, default is 100. Relevant only if ``kind='raw'``. axis : {0, 1}, optional Axis along wich the computation is done. Default is 0. dtype : np.dtype, optional The type of the output array. If `dtype` is not given, infer the data type from `X` input. Returns ------- np.ndarray[dtype, ndim=1 or 2] Returns along time axis. See Also -------- perf_returns, perf_index Examples -------- >>> X = np.array([10., 12., 15., 14., 16., 18., 16.]) >>> S = np.array([1., 1., 1., 0., 1., 1., -1.]) >>> returns_strat(X, S, base=10., kind='raw') array([ 0., 2., 3., -0., 2., 2., 2.]) >>> returns_strat(X, S) array([ 0. , 0.2 , 0.25 , -0. , 0.14285714, 0.125 , 0.11111111]) """ if S is None: S = np.ones(X.shape) elif S.ndim < X.ndim and S.shape[0] == X.shape[0]: S = S.reshape(S.shape + (1,) * (X.ndim - S.ndim)) elif S.shape == X.shape: pass else: raise ValueError('S and X could not be broadcast, must have the same ' 'shape or axis {} of same size and S.ndim < X.ndim ' '(but S:{} and X:{})'.format(axis, S.shape, X.shape)) R = np.zeros(X.shape) X = base * X / X[0] if kind == 'raw': R[1:] = X[1:] - X[:-1] elif kind == 'pct': R[1:] = X[1:] / X[:-1] - 1 return R * S
[docs] @WrapperArray('dtype', 'axis', 'null', 'ddof', min_size=2) def sharpe(X: NDArray, rf: float = 0, period: int = 252, log: bool = False, axis: int = 0, dtype=None, ddof: int = 0) -> NDArray: r""" Compute the Sharpe ratio for each `X`' series. Annualized excess return per unit of volatility — the most widely used risk-adjusted performance metric. Higher is better; ratios above 1 are usually considered good and above 2 excellent for long-horizon strategies. Note that the Sharpe ratio penalizes both upside and downside volatility symmetrically; use the Sortino or Calmar ratio (:func:`calmar`) when only downside risk should be penalized. The ``period`` argument controls annualization (252 for daily trading data, 12 for monthly, etc.). For a rolling estimate, see :func:`roll_sharpe`. Notes ----- Sharpe ratio [7]_ is computed as the annualized expected returns (:func:`~annual_return`) minus the risk-free rate (noted :math:`rf`) over the annualized volatility of returns (:func:`~annual_volatility`) such that: .. math:: sharpeRatio = \frac{E(R) - rf}{\sqrt{period \times Var(R)}} \\ \\ where, :math:`R_1 = 0` and :math:`R_{2:T} = \begin{cases}ln(\frac{X_{2:T}} {X_{1:T-1}}) \text{, if log=True}\\ \frac{X_{2:T}}{X_{1:T-1}} - 1 \text{, otherwise} \\ \end{cases}` Parameters ---------- X : np.ndarray[dtype, ndim=1 or 2] Time-series of prices, performances or index. rf : float, optional Means the annualized risk-free rate, default is 0. period : int, optional Number of period per year, default is 252 (trading days). log : bool, optional If true compute sharpe with the formula for log-returns, default is False. axis : {0, 1}, optional Axis along wich the computation is done. Default is 0. dtype : np.dtype, optional The type of the output array. If `dtype` is not given, infer the data type from `X` input. ddof : int, optional Means Delta Degrees of Freedom, the divisor used in calculations is ``T - ddof``, where ``T`` represents the number of elements in time axis. Default is 0. Returns ------- dtype or np.ndarray[dtype, ndim=1] Value of Sharpe ratio for each series. References ---------- .. [7] https://en.wikipedia.org/wiki/Sharpe_ratio Examples -------- Assume a series X of monthly prices: >>> X = np.array([70, 100, 80, 120, 160, 80]).astype(np.float64) >>> sharpe(X, period=12) 0.24475518072327812 >>> sharpe(X.reshape([6, 1]), period=12) array([0.24475518]) See Also -------- mdd, calmar, drawdown, roll_sharpe """ ret = _annual_return(X, period, ddof) vol = _annual_volatility(X, period, log, axis, ddof) if (vol == 0.).any(): res = ret - rf res[vol == 0.] = np.inf res[vol != 0.] = res[vol != 0.] / vol[vol != 0.] return res return (ret - rf) / vol
@WrapperArray('dtype', 'axis', 'null', 'ddof', min_size=2) def sortino( X: NDArray, rf: float = 0, period: int = 252, log: bool = False, axis: int = 0, dtype=None, ddof: int = 0, ) -> NDArray: r""" Compute the Sortino ratio for each `X`' series. Annualized excess return per unit of *downside* volatility. Unlike the Sharpe ratio (:func:`sharpe`), only negative returns contribute to the denominator, so strategies that generate frequent large gains are not penalized for their upside variance. Notes ----- The Sortino ratio is computed as the annualized expected return minus the risk-free rate divided by the annualized downside deviation: .. math:: sortinoRatio = \frac{E(R) - rf}{\sqrt{period \times Var(R^{-})}} where :math:`R^{-}_t = \min(R_t, 0)` and :math:`R` is defined as for :func:`sharpe`. Parameters ---------- X : np.ndarray[dtype, ndim=1 or 2] Time-series of prices, performances or index. rf : float, optional Annualized risk-free rate. Default is 0. period : int, optional Number of periods per year. Default is 252 (trading days). log : bool, optional If True, compute returns as log-returns. Default is False. axis : {0, 1}, optional Axis along which the computation is done. Default is 0. dtype : np.dtype, optional Output array dtype. Inferred from ``X`` if not given. ddof : int, optional Delta Degrees of Freedom. Default is 0. Returns ------- dtype or np.ndarray[dtype, ndim=1] Sortino ratio for each series. Returns ``inf`` when downside volatility is zero (all returns are non-negative). Examples -------- Assume a series X of monthly prices: >>> X = np.array([70, 100, 80, 120, 160, 80]).astype(np.float64) >>> sortino(X, period=12) 0.4742428587192754 >>> sortino(X.reshape([6, 1]), period=12) array([0.47424286]) See Also -------- sharpe, calmar, mdd """ R = _compute_returns(X, log) ret = _annual_return(X, period, ddof) downside_vol = np.sqrt(period) * np.std(np.where(R < 0, R, 0.), axis=axis, ddof=ddof) if (downside_vol == 0.).any(): res = ret - rf res[downside_vol == 0.] = np.inf res[downside_vol != 0.] = res[downside_vol != 0.] / downside_vol[downside_vol != 0.] return res return (ret - rf) / downside_vol @WrapperArray('axis') def directional_accuracy( y_true: NDArray, y_pred: NDArray, axis: int = 0, ) -> float: r""" Compute the directional accuracy of a prediction. Fraction of periods where the predicted direction (sign) matches the true direction. A value of 1.0 means perfect directional alignment; 0.5 is random; 0.0 means systematically wrong direction. Notes ----- .. math:: directionalAccuracy = \frac{1}{T} \sum_{t=1}^{T} \mathbf{1}[\text{sign}(\hat{y}_t) = \text{sign}(y_t)] Parameters ---------- y_true : np.ndarray[ndim=1 or 2, dtype] Vector of true values (returns or price changes). y_pred : np.ndarray[ndim=1 or 2, dtype] Vector of predicted values. axis : {0, 1}, optional Axis along which the computation is done. Default is 0. Returns ------- float or np.ndarray[ndim=1, float] Directional accuracy between 0 and 1. Examples -------- >>> y_true = np.array([1., .5, -.5, .8, -.2]) >>> y_pred = np.array([.5, .2, -.5, .1, .0]) >>> directional_accuracy(y_true, y_pred) 0.8 See Also -------- accuracy """ return np.mean(np.sign(y_true) == np.sign(y_pred), axis=axis)
[docs] @WrapperArray('dtype', 'axis', 'window') def z_score(X: NDArray, w: int = 0, kind: str = 's', axis: int = 0, dtype=None) -> NDArray: r""" Compute the Z-score of each `X`' series. Notes ----- Compute the z-score function for a specific average and standard deviation function such that: .. math:: z = \frac{X_t - \mu_t}{\sigma_t} Where :math:`\mu_t` is the average and :math:`\sigma_t` is the standard deviation. Parameters ---------- X : np.ndarray[dtype, ndim=1 or 2] Series of index, prices or returns. w : int, optional Size of the lagged window of the moving averages, must be positive. If ``w is None`` or ``w=0``, then ``w=X.shape[axis]``. Default is None. kind : {'e', 's', 'w'} - If 'e' then use exponential moving average, see :func:`~fynance.features.momentums.ema` for details. - If 's' (default) then use simple moving average, see :func:`~fynance.features.momentums.sma` for details. - If 'w' then use weighted moving average, see :func:`~fynance.features.momentums.wma` for details. axis : {0, 1}, optional Axis along wich the computation is done. Default is 0. dtype : np.dtype, optional The type of the output array. If `dtype` is not given, infer the data type from `X` input. Returns ------- dtype or np.ndarray[dtype, ndim=1] Value of Z-score for each series. Examples -------- >>> X = np.array([70, 100, 80, 120, 160, 80]).astype(np.float64) >>> z_score(X, w=3, kind='e') -1.0443574118998766 >>> z_score(X, w=3) -1.224744871391589 >>> z_score(X.reshape([6, 1]), w=3) array([-1.22474487]) See Also -------- roll_z_score, mdd, calmar, drawdown, sharpe """ # TODO : make a more efficient function if kind == 'e': w = 1 - 2 / (1 + w) avg = _handler_ma[kind.lower()](X, w) std = _handler_mstd[kind.lower()](X, w) std[std == 0.] = 1. z = (X - avg) / std return z[-1]
# =========================================================================== # # Rolling metrics # # =========================================================================== # # TODO : rolling perf metric # TODO : rolling diversified ratio
[docs] @WrapperArray('dtype', 'axis', 'window', 'ddof', min_size=2) def roll_annual_return(X: NDArray, period: int = 252, w: int | None = None, axis: int = 0, dtype=None, ddof: int = 0) -> NDArray: r""" Compute rolling compouned annual returns of each `X`' series. The annualised return [1]_ is the process of converting returns on a whole period to returns per year. Notes ----- The rolling annual compouned returns is computed such that :math:`\forall t \in [1: T]`: .. math:: annualReturn_t = \frac{X_t}{X_1}^{\frac{period}{t}} - 1 Parameters ---------- X : np.ndarray[dtype, ndim=1 or 2] Time-series of price, performance or index. period : int, optional Number of period per year, default is 252 (trading days per year). w : int, optional Size of the lagged window of the rolling function, must be positive. If ``w is None`` or ``w=0``, then ``w=X.shape[axis]``. Default is None. axis : {0, 1}, optional Axis along wich the computation is done. Default is 0. dtype : np.dtype, optional The type of the output array. If `dtype` is not given, infer the data type from `X` input. ddof : int, optional Means Delta Degrees of Freedom, the divisor used in calculations is ``t - ddof``, where ``t`` represents the number of elements in time axis. Default is 0. Returns ------- np.ndarray[dtype, ndim=1 or 2] Values of rolling compouned annual returns of each series. References ---------- .. [1] https://en.wikipedia.org/wiki/Rate_of_return#Annualisation Examples -------- Assume series of monthly prices: >>> X = np.array([100, 110, 80, 120, 160, 108]).astype(np.float64) >>> roll_annual_return(X, period=12) array([ 0. , 0.771561 , -0.5904 , 0.728 , 2.08949828, 0.1664 ]) >>> X = np.array([[100, 101], [80, 81], [110, 108]]).astype(np.float64) >>> roll_annual_return(X, period=12, axis=1) array([[ 0. , 0.06152015], [ 0. , 0.07738318], [ 0. , -0.10425081]]) See Also -------- mdd, drawdown, sharpe, annual_volatility """ return _roll_annual_return(X, period, w, ddof)
def _roll_annual_return(X, period, w, ddof): if ddof >= w: raise ValueError( 'size of the lagged window (w={}) must be strictly greater than ' 'degree of freedom (ddof={})'.format(w, ddof) ) elif (X[0] == 0).any(): raise ValueError('initial value X[0] cannot be null.') elif len(X.shape) == 2: return np.asarray(roll_annual_return_cy_2d(X, period, w, ddof)) return np.asarray(roll_annual_return_cy_1d(X, period, w, ddof))
[docs] @WrapperArray('dtype', 'axis', 'null', 'window', 'ddof', min_size=3) def roll_annual_volatility( X: NDArray, period: int = 252, log: bool = True, w: int | None = None, axis: int = 0, dtype=None, ddof: int = 0, ) -> NDArray: r""" Compute the annualized volatility of each `X`' series. In finance, volatility is the degree of variation of a trading price series over time as measured by the standard deviation of logarithmic returns [2]_. Notes ----- The rolling annualized volatility of returns is computed such that :math:`\forall t \in [1, T]`: .. math:: annualVolatility_t = \sqrt{period \times Var(R_{1:t})} \\ \\ Where, :math:`R_1 = 0` and :math:`R_{2:t} = \begin{cases}ln(\frac{X_{2:t}} {X_{1:t-1}}) \text{, if log=True}\\ \frac{X_{2:t}}{X_{1:t-1}} - 1 \text{, otherwise} \\ \end{cases}`. Parameters ---------- X : np.ndarray[dtype, ndim=1 or 2] Time-series of price, performance or index. period : int, optional Number of period per year, default is 252 (trading days per year). log : bool, optional - If True then logarithmic returns are computed. - Else then returns in percentage are computed. w : int, optional Size of the lagged window of the rolling function, must be positive. If ``w is None`` or ``w=0``, then ``w=X.shape[axis]``. Default is None. axis : {0, 1}, optional Axis along wich the computation is done. Default is 0. dtype : np.dtype, optional The type of the output array. If `dtype` is not given, infer the data type from `X` input. ddof : int, optional Means Delta Degrees of Freedom, the divisor used in calculations is ``t - ddof``, where ``t`` represents the number of elements in time axis. Default is 0. Returns ------- dtype or np.ndarray([dtype, ndim=1 or 2]) Rolling annualized volatility for each series. References ---------- .. [2] https://en.wikipedia.org/wiki/Volatility_(finance) Examples -------- Assume series of monthly prices: >>> X = np.array([100, 110, 105, 110, 120, 108]).astype(np.float64) >>> roll_annual_volatility(X, period=12, log=False, ddof=1) array([0. , 0.24494897, 0.25777176, 0.21655755, 0.21313847, 0.27344193]) >>> roll_annual_volatility(X.reshape([6, 1]), period=12, log=False) array([[0. ], [0.17320508], [0.21046976], [0.18754434], [0.19063685], [0.24961719]]) See Also -------- mdd, drawdown, sharpe, annual_return """ return _roll_annual_volatility(X, period, log, w, axis, ddof)
def _roll_annual_volatility(X, period, log, w, axis, ddof): if ddof >= w: raise ValueError( 'size of the lagged window (w={}) must be strictly greater than ' 'degree of freedom (ddof={})'.format(w, ddof) ) elif len(X.shape) == 2: return np.asarray(roll_annual_volatility_cy_2d( X, period, int(log), w, ddof )) return np.asarray(roll_annual_volatility_cy_1d( X, period, int(log), w, ddof ))
[docs] @WrapperArray('dtype', 'axis', 'window', 'ddof', min_size=2) def roll_calmar(X: NDArray, period: float = 252., w: int | None = None, axis: int = 0, dtype=None, ddof: int = 0) -> NDArray: r""" Compute the rolling Calmar ratio of each `X`' series. Notes ----- Calmar ratio [3]_ is the rolling compouned annual return (:func:`~fynance.features.metrics.roll_annual_return`) over the rolling maximum drawdown (:func:`~fynance.features.metrics.roll_mdd`). Let :math:`T` the number of time observations, DD the vector of drawdown, :math:`\forall t \in [1:T]`: .. math:: calmarRatio_t = \frac{annualReturn_t}{MDD_t} \\ \\ With, :math:`annualReturn_t = \frac{X_t}{X_1}^{\frac{period}{t}} - 1` and :math:`MDD_t = max(DD_t)`, where :math:`DD_t = 1 - \frac{X_t}{max(X_{1:t})}`. Parameters ---------- X : np.ndarray[dtype, ndim=1 or 2] Time-series of price, performance or index. period : int, optional Number of period per year, default is 252 (trading days per year). w : int, optional Size of the lagged window of the rolling function, must be positive. If ``w is None`` or ``w=0``, then ``w=X.shape[axis]``. Default is None. axis : {0, 1}, optional Axis along wich the computation is done. Default is 0. dtype : np.dtype, optional The type of the output array. If `dtype` is not given, infer the data type from `X` input. ddof : int, optional Means Delta Degrees of Freedom, the divisor used in calculations is ``t - ddof``, where ``t`` represents the number of elements in time axis. Default is 0. Returns ------- np.ndarray[dtype, ndim=1 or 2] Series of rolling Calmar ratio. References ---------- .. [3] https://en.wikipedia.org/wiki/Calmar_ratio Examples -------- Assume a monthly series of prices: >>> X = np.array([70, 100, 80, 120, 160, 80]).astype(np.float64) >>> roll_calmar(X, period=12) array([ 0. , 0. , 3.52977926, 20.18950437, 31.35989887, 0.6122449 ]) See Also -------- roll_mdd, roll_sharpe, calmar """ ret = _roll_annual_return(X, period, w, ddof) mdd = _roll_mdd(X, w, False) calmar = np.zeros(X.shape) slice_bool = (mdd != 0) calmar[slice_bool] = ret[slice_bool] / mdd[slice_bool] return calmar
[docs] @WrapperArray('dtype', 'axis', 'window') def roll_drawdown(X: NDArray, w: int | None = None, raw: bool = False, axis: int = 0, dtype=None) -> NDArray: r""" Measures the rolling drawdown of each `X`' series. Function to compute measure of the decline from a historical peak in some variable [5]_ (typically the cumulative profit or total open equity of a financial trading strategy). Notes ----- Let DD^w the drawdown vector with a lagged window of size `w`: .. math:: DD^w_t =\begin{cases} max(X_{t - w + 1:t}) - X_t \text{, if raw=True} \\ 1 - \frac{X_t}{max(X_{t - w + 1:t})} \text{, otherwise} \\ \end{cases} Parameters ---------- X : np.ndarray[dtype, ndim=1 or 2] Time-series of prices, performances or index. Must be positive values. w : int, optional Size of the lagged window of the rolling function, must be positive. If ``w is None`` or ``w=0``, then ``w=X.shape[axis]``. Default is None. raw : bool, optional - If True then compute the raw drawdown. - Else (default) compute the drawdown in percentage. axis : {0, 1}, optional Axis along wich the computation is done. Default is 0. dtype : np.dtype, optional The type of the output array. If `dtype` is not given, infer the data type from `X` input. Returns ------- np.ndarray[dtype, ndim=1 or 2] Series of drawdown for each series. References ---------- .. [5] https://en.wikipedia.org/wiki/Drawdown_(economics) Examples -------- >>> X = np.array([70, 100, 80, 120, 160, 80]).astype(np.float64) >>> roll_drawdown(X) array([0. , 0. , 0.2, 0. , 0. , 0.5]) >>> roll_drawdown(X.reshape([6, 1])).T array([[0. , 0. , 0.2, 0. , 0. , 0.5]]) >>> roll_drawdown(X, raw=True) array([ 0., 0., 20., 0., 0., 80.]) >>> X = np.array([100, 80, 70, 75, 110, 80]).astype(np.float64) >>> roll_drawdown(X, raw=True, w=3) array([ 0., 20., 30., 5., 0., 30.]) See Also -------- mdd, calmar, sharpe, roll_mdd """ return _roll_drawdown(X, w, raw)
def _roll_drawdown(X, w, raw): if (X[0] == 0).any() and not raw: warn( 'Cannot compute drawdown in percentage without initial values ' 'X[0] strictly positive.', category=UserWarning, stacklevel=2, ) raw = True if len(X.shape) == 2: return np.asarray(roll_drawdown_cy_2d(X, w, int(raw))) return np.asarray(roll_drawdown_cy_1d(X, w, int(raw)))
[docs] @WrapperArray('dtype', 'axis', 'window') def roll_mad(X: NDArray, w: int | None = None, axis: int = 0, dtype=None) -> NDArray: """ Compute rolling Mean Absolut Deviation for each `X`' series. Compute the moving average of the absolute value of the distance to the moving average [6]_. Parameters ---------- X : np.ndarray[dtype, ndim=1 or 2] Time series (price, performance or index). w : int, optional Size of the lagged window of the rolling function, must be positive. If ``w is None`` or ``w=0``, then ``w=X.shape[axis]``. Default is None. axis : {0, 1}, optional Axis along wich the computation is done. Default is 0. dtype : np.dtype, optional The type of the output array. If `dtype` is not given, infer the data type from `X` input. Returns ------- np.ndarray[dtype, ndim=1 or 2] Series of mean absolute deviation. References ---------- .. [6] https://en.wikipedia.org/wiki/Average_absolute_deviation Examples -------- >>> X = np.array([70, 100, 90, 110, 150, 80]) >>> roll_mad(X, dtype=np.float64) array([ 0. , 15. , 11.11111111, 12.5 , 20.8 , 20. ]) >>> X = np.array([60, 100, 80, 120, 160, 80]).astype(np.float64) >>> roll_mad(X, w=3, dtype=np.float64) array([ 0. , 20. , 13.33333333, 13.33333333, 26.66666667, 26.66666667]) See Also -------- mad """ if len(X.shape) == 2: return np.asarray(roll_mad_cy_2d(X, w)) return np.asarray(roll_mad_cy_1d(X, w))
[docs] @WrapperArray('dtype', 'axis', 'window') def roll_mdd(X: NDArray, w: int | None = None, raw: bool = False, axis: int = 0, dtype=None) -> NDArray: """ Compute the rolling maximum drawdown for each `X`' series. Where drawdown is the measure of the decline from a historical peak in some variable [5]_ (typically the cumulative profit or total open equity of a financial trading strategy). Parameters ---------- X : np.ndarray[dtype, ndim=1 or 2] Time series (price, performance or index). w : int, optional Size of the lagged window of the rolling function, must be positive. If ``w is None`` or ``w=0``, then ``w=X.shape[axis]``. Default is None. raw : bool, optional - If True then compute the raw drawdown. - Else (default) compute the drawdown in percentage. axis : {0, 1}, optional Axis along wich the computation is done. Default is 0. dtype : np.dtype, optional The type of the output array. If `dtype` is not given, infer the data type from `X` input. Returns ------- np.ndrray[dtype, ndim=1 or 2] Series of rolling maximum drawdown for each series. References ---------- .. [5] https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp Examples -------- >>> X = np.array([70, 100, 80, 120, 160, 80]) >>> roll_mdd(X, dtype=np.float64) array([0. , 0. , 0.2, 0.2, 0.2, 0.5]) >>> roll_mdd(X, w=3, dtype=np.float64) array([0. , 0. , 0.2, 0.2, 0. , 0.5]) >>> X = np.array([100, 80, 70, 75, 110, 80]).astype(np.float64) >>> roll_mdd(X, raw=True, w=3, dtype=np.float64) array([ 0., 20., 30., 10., 0., 30.]) See Also -------- mdd, roll_calmar, roll_sharpe, drawdown """ return _roll_mdd(X, w, raw)
def _roll_mdd(X, w, raw): if len(X.shape) == 2: return np.asarray(roll_mdd_cy_2d(X, w, int(raw))) return np.asarray(roll_mdd_cy_1d(X, w, int(raw)))
[docs] @WrapperArray('dtype', 'axis', 'window', 'ddof', min_size=2) def roll_sharpe( X: NDArray, rf: float = 0, period: int = 252, w: int | None = None, log: bool = False, axis: int = 0, dtype=None, ddof: int = 0, ) -> NDArray: r""" Compute rolling sharpe ratio of each `X`' series. Notes ----- Sharpe ratio [7]_ is computed as the rolling annualized expected returns (:func:`~roll_annual_return`) minus the risk-free rate (noted :math:`rf`) over the rolling annualized volatility of returns (:func:`~roll_annual_volatility`) such that :math:`\forall t \in [1:T]`: .. math:: sharpeRatio_t = \frac{E(R | R_{1:t}) - rf_t}{\sqrt{period \times Var(R | R_{1:t})}} Where, :math:`R_1 = 0` and :math:`R_{2:T} = \begin{cases}ln(\frac{X_{2:T}} {X_{1:T-1}}) \text{, if log=True}\\ \frac{X_{2:T}}{X_{1:T-1}} - 1 \text{, otherwise} \\ \end{cases}`. Parameters ---------- X : np.ndarray[dtype, ndim=1 or 2] Time-series of prices, performances or index. rf : float or np.ndarray[dtype, ndim=1 or 2], optional Means the annualized risk-free rate, default is 0. If an array is passed, it must be of the same shape than ``X``. period : int, optional Number of period per year, default is 252 (trading days). w : int, optional Size of the lagged window of the rolling function, must be positive. If ``w is None`` or ``w=0``, then ``w=X.shape[axis]``. Default is None. log : bool, optional If true compute sharpe with the formula for log-returns, default is False. axis : {0, 1}, optional Axis along wich the computation is done. Default is 0. dtype : np.dtype, optional The type of the output array. If `dtype` is not given, infer the data type from `X` input. ddof : int, optional Means Delta Degrees of Freedom, the divisor used in calculations is ``T - ddof``, where ``T`` represents the number of elements in time axis. Default is 0. Returns ------- np.ndarray[dtype, ndim=1 or 2] Serires of rolling Sharpe ratio. References ---------- .. [7] https://en.wikipedia.org/wiki/Sharpe_ratio Examples -------- Assume a monthly series of prices: >>> X = np.array([70, 100, 80, 120, 160, 80]).astype(np.float64) >>> roll_sharpe(X, period=12) array([ 0. , 10.10344078, 0.77721579, 3.99243019, 6.754557 , 0.24475518]) See Also -------- roll_calmar, sharpe, roll_mdd """ ret = _roll_annual_return(X, period, w, ddof) vol = _roll_annual_volatility(X, period, log, w, axis, ddof) sharpe = np.zeros(X.shape) slice_bool = (vol != 0) if isinstance(rf, float) or isinstance(rf, int): _rf = rf elif isinstance(rf, np.ndarray) and rf.shape[0] != X.shape[0]: msg_prefix = 'rf must be ' raise ArraySizeError(X.shape[0], msg_prefix=msg_prefix) else: _rf = rf[slice_bool] sharpe[slice_bool] = (ret[slice_bool] - _rf) / vol[slice_bool] return sharpe
[docs] @WrapperArray('dtype', 'axis', 'window') def roll_z_score(X: NDArray, w: int | None = None, kind: str = 's', axis: int = 0, dtype=None) -> NDArray: r""" Compute vector of rolling/moving Z-score function. Notes ----- Compute for each observation the z-score function for a specific moving average function such that :math:`\forall t \in [1:T]`: .. math:: z_t = \frac{X_t - \mu_t}{\sigma_t} Where :math:`\mu_t` is the moving average and :math:`\sigma_t` is the moving standard deviation. Parameters ---------- X : np.ndarray[dtype, ndim=1 or 2] Series of index, prices or returns. w : int, optional Size of the lagged window of the moving averages, must be positive. If ``w is None`` or ``w=0``, then ``w=X.shape[axis]``. Default is None. kind : {'e', 's', 'w'} - If 'e' then use exponential moving average, see :func:`~fynance.features.momentums.ema` for details. - If 's' (default) then use simple moving average, see :func:`~fynance.features.momentums.sma` for details. - If 'w' then use weighted moving average, see :func:`~fynance.features.momentums.wma` for details. axis : {0, 1}, optional Axis along wich the computation is done. Default is 0. dtype : np.dtype, optional The type of the output array. If `dtype` is not given, infer the data type from `X` input. Returns ------- np.ndarray[dtype, ndim=1 or 2] Vector of Z-score at each period. Examples -------- >>> X = np.array([70, 100, 80, 120, 160, 80]).astype(np.float64) >>> roll_z_score(X, w=3, kind='e') array([ 0. , 1.41421356, -0.32444284, 1.30806216, 1.27096675, -1.04435741]) >>> roll_z_score(X, w=3) array([ 0. , 1. , -0.26726124, 1.22474487, 1.22474487, -1.22474487]) See Also -------- z_score, roll_mdd, roll_calmar, roll_mad, roll_sharpe """ if kind == 'e': w = 1 - 2 / (1 + w) avg = _handler_ma[kind.lower()](X, w) std = _handler_mstd[kind.lower()](X, w) std[std == 0.] = 1. z = (X - avg) / std return z
# =========================================================================== # # old scripts # # =========================================================================== # def _roll_annual_return_py(X, period, w, ddof): """ Old function. """ if (X[0] == 0).any(): raise ValueError('initial value X[0] cannot be null.') cum_ret = np.zeros(X.shape) cum_ret[: w] = X[: w] / X[0] cum_ret[w:] = X[w:] / X[: -w] if (cum_ret < 0).any(): raise ValueError('all values of X must be of the same sign.') T = X.shape[0] power = period / np.arange(1, T - ddof + 1, dtype=np.float64) if len(X.shape) == 2: power = power.reshape([T, 1]) sign = np.sign(X[0]) anu_ret = np.zeros(X.shape) anu_ret[ddof:] = sign * np.float_power(cum_ret[ddof:], power) - 1. return anu_ret def _roll_annual_volatility_py(X, period, log, w, axis, ddof): """ Old function. """ shape = X.shape T = shape[0] R = np.zeros(shape) anu_vol = np.zeros(shape) if log: R[1:] = np.log(X[1:] / X[:-1]) else: R[1:] = X[1:] / X[:-1] - 1. for t in range(ddof + 1, T): t0 = max(0, t - w) anu_vol[t] = np.std(R[t0:t + 1], axis=axis, ddof=ddof) return np.sqrt(period) * anu_vol # =========================================================================== # # Tests # # =========================================================================== # if __name__ == '__main__': import doctest doctest.testmod()