#!/usr/bin/env python3
# coding: utf-8
""" Return and performance metrics (annualized return, P&L paths). """
from __future__ import annotations
# Built-in packages
# Third-party packages
import numpy as np
from numpy.typing import NDArray
# Local packages
from fynance._wrappers import WrapperArray
from fynance.features._metrics_helpers import * # noqa: F401,F403
from fynance.features.metrics_cy import *
__all__ = ['annual_return', 'perf_index', 'perf_returns', 'roll_annual_return']
[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)
[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) # type: ignore[assignment]
else:
raise ValueError(
f"unknown kind {kind!r} of returns, only 'raw', 'log', 'pct' "
"are supported"
)
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)
[docs]
@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', '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)