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)
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
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
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
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
def derive_returns(values): values = to_time_series(values) return values.pct_change() if not is_returns else values
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)
def etf(etf_raw: pd.DataFrame): return to_time_series(etf_raw)