def test_to_returns_and_prices(etf_raw, expected_returns):
    ts = to_time_series(etf_raw)

    ret = to_returns(ts)

    assert_array_almost_equal(ret, expected_returns)

    prices = to_prices(ret, start=ts.iloc[0, :])
    assert_array_almost_equal(prices, ts)
def test_to_log_returns_and_prices(etf_raw, expected_log_returns):
    ts = to_time_series(etf_raw)

    log_ret = to_returns(ts, log=True)

    assert_array_almost_equal(log_ret, expected_log_returns)

    prices = to_prices(log_ret, start=ts.iloc[0, :], log=True)
    assert_array_almost_equal(prices, ts)
Exemple #3
0
def drawdown(data: TimeSeriesData,
             weights: Vector = None,
             geometric=True,
             rebalance=True) -> pd.Series:
    """
    Calculates the drawdown at each time instance.

    If data is DataFrame-like, weights must be specified. If data is Series-like, weights
    can be left empty.

    Parameters
    ----------
    data
        The assets returns vector or matrix

    weights
        Weights of the portfolio. This must be 1 dimensional and must match the dimension of the data's
        last axis.

    geometric
        If True, calculates the geometric mean, otherwise, calculates the arithmetic mean.

    rebalance
        If True, portfolio is assumed to be rebalanced at every step.

    Returns
    -------
    Series
        Drawdown at each time instance

    Examples
    --------
    >>> from perfana.datasets import load_hist
    >>> from perfana.core import drawdown
    >>> hist = load_hist().iloc[:, :7]
    >>> weights = [0.25, 0.18, 0.24, 0.05, 0.04, 0.13, 0.11]
    >>> drawdown(hist, weights).min()
    -0.4007984968456346
    >>> drawdown(hist.iloc[:, 0]).min()
    -0.5491340502573534

    .. plot:: plots/core_drawdown.py
        :include-source:
    """
    data = to_time_series(data)
    weights = np.ravel(weights)

    assert data.ndim in (1, 2), "returns can only be a series or a dataframe"
    if data.ndim == 2:
        assert weights is not None, "weight must be specified if returns is a dataframe"
        weights = np.ravel(weights)

        if rebalance:
            cum_ret = (
                data @ weights +
                1).cumprod() if geometric else (data @ weights).cumsum() + 1
        else:
            cum_ret = (data + 1).cumprod(
            ) @ weights if geometric else data.cumsum() @ weights + 1
    else:
        cum_ret = (data + 1).cumprod() if geometric else data.cumsum() + 1

    dd = cum_ret / cum_ret.expanding().max() - 1
    setattr(dd, "is_drawdown", True)
    return dd
Exemple #4
0
def annualized_returns(r: TimeSeriesData,
                       freq: Optional[str] = None,
                       geometric=True) -> Union[float, pd.Series]:
    r"""
    Calculates the annualized returns from the data

    The formula for annualized geometric returns is formulated by raising the compound return to the number of
    periods in a year, and taking the root to the number of total observations:

    .. math::
        \prod_i^N(1 + r_i)^{\frac{s}{N}} - 1

    where :math:`s` is the number of observations in a year, and :math:`N` is the total number of observations.

    For simple returns (geometric=FALSE), the formula is:

    .. math::
        \frac{s}{N} \sum^N_i r_i


    Parameters
    ----------
    r
        Numeric returns series or data frame

    freq
        Frequency of the data. Use one of daily, weekly, monthly, quarterly, semi-annually, yearly

    geometric
        If True, calculates the geometric returns. Otherwise, calculates the arithmetic returns

    Returns
    -------
    float or Series
        Annualized returns

    Examples
    --------
    >>> from perfana.datasets import load_etf
    >>> from perfana.core import annualized_returns
    # Get returns starting from the date where all etf has data
    >>> etf = load_etf().dropna().pa.to_returns().dropna()
    VBK    0.091609
    BND    0.036224
    VTI    0.081203
    VWO    0.027670
    dtype: float64
    >>> annualized_returns(etf.VWO)
    0.02767037698144148
    """
    r = to_time_series(r).dropna()
    if freq is None:
        freq = r.pa.frequency

    scale = freq_to_scale(freq)

    if geometric:
        d = (r + 1).prod()
        return np.sign(d) * np.abs(d)**(scale / len(r)) - 1
    else:  # arithmetic mean
        return r.mean() * scale
Exemple #5
0
def _to_time_series(r, prefix):
    r = to_time_series(r)
    if isinstance(r, pd.Series):
        r = pd.DataFrame(r.rename(r.name or prefix))
    return r
Exemple #6
0
def excess_returns(ra: TimeSeriesData,
                   rb: TimeSeriesData,
                   freq: Optional[str] = None,
                   geometric=True) -> TimeSeriesData:
    r"""
    An average annualized excess return is convenient for comparing excess returns

    Excess returns is calculated by first annualizing the asset returns and benchmark returns stream. See the docs for
    `annualized_returns()` for more details. The geometric returns formula is:

    .. math::
        r_g = \frac{r_a - r_b}{1 + r_b}

    The arithmetic excess returns formula is:

    .. math::
        r_g = r_a - r_b

    Returns calculation will be truncated by the one with the shorter length. Also, annualized returns are calculated
    by the geometric annualized returns in both cases

    Parameters
    ----------
    ra
        The assets returns vector or matrix

    rb
        The benchmark returns. If this is a vector and the asset returns is a matrix, then all assets returns (columns)
        will be compared against this single benchmark. Otherwise, if this is a matrix, then assets will be compared
        to each individual benchmark (i.e. column for column)

    freq
        Frequency of the data. Use one of [daily, weekly, monthly, quarterly, semi-annually, yearly]

    geometric
        If True, calculates the geometric excess returns. Otherwise, calculates the arithmetic excess returns

    Returns
    -------
    TimeSeriesData
        Excess returns of each strategy against benchmark

    Examples
    --------
    >>> from perfana.datasets import load_etf
    >>> from perfana.core import excess_returns
    # Get returns starting from the date where all etf has data
    >>> etf = load_etf().dropna().pa.to_returns().dropna()
    >>> excess_returns(etf, etf.VBK)
    VBK    0.000000
    BND   -0.050737
    VTI   -0.009533
    VWO   -0.058573
    dtype: float64
    """
    ra = to_time_series(ra).dropna()
    rb = to_time_series(rb).dropna()

    n = min(len(ra), len(rb))
    ra, rb = ra.iloc[:n], rb.iloc[:n]

    if ra.ndim == rb.ndim and ra.shape != rb.shape:
        raise ValueError(
            'The shapes of the asset and benchmark returns do not match!')

    freq = _determine_frequency(ra, rb, freq)

    ra = annualized_returns(ra, freq)
    rb = annualized_returns(rb, freq)

    return (ra - rb) / (1 + rb) if geometric else ra - rb
Exemple #7
0
 def derive_returns(values):
     values = to_time_series(values)
     return values.pct_change() if not is_returns else values
Exemple #8
0
    def derive_rolling_returns(values):
        values = to_time_series(values)
        if not is_returns:
            values = values.pct_change() + 1

        return values.rolling(days_in_duration(duration)).apply(lambda x: x.prod(), raw=False)
Exemple #9
0
def etf(etf_raw: pd.DataFrame):
    return to_time_series(etf_raw)