From cebd1383b3e523102dbf72d86cf84eff62c54cf3 Mon Sep 17 00:00:00 2001 From: raotushar Date: Tue, 23 Jan 2024 19:25:59 +0530 Subject: [PATCH 1/3] Fix CAGR calculation and add periods parameter to relevant functions --- quantstats/reports.py | 12 ++++++------ quantstats/stats.py | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/quantstats/reports.py b/quantstats/reports.py index de34b9c3..129676ab 100644 --- a/quantstats/reports.py +++ b/quantstats/reports.py @@ -836,7 +836,7 @@ def metrics( else: metrics["Total Return %"] = (df.sum() * pct).map("{:,.2f}".format) - metrics["CAGR﹪%"] = _stats.cagr(df, rf, compounded) * pct + metrics["CAGR﹪%"] = _stats.cagr(df, rf, compounded, win_year) * pct metrics["~~~~~~~~~~~~~~"] = blank @@ -923,7 +923,7 @@ def metrics( elif isinstance(returns, _pd.DataFrame): metrics["Volatility (ann.) %"] = ret_vol - metrics["Calmar"] = _stats.calmar(df, prepare_returns=False) + metrics["Calmar"] = _stats.calmar(df, prepare_returns=False, periods=win_year) metrics["Skew"] = _stats.skew(df, prepare_returns=False) metrics["Kurtosis"] = _stats.kurtosis(df, prepare_returns=False) @@ -991,15 +991,15 @@ def metrics( metrics["1Y %"] = comp_func(df[df.index >= d]) * pct d = today - relativedelta(months=35) - metrics["3Y (ann.) %"] = _stats.cagr(df[df.index >= d], 0.0, compounded) * pct + metrics["3Y (ann.) %"] = _stats.cagr(df[df.index >= d], 0.0, compounded, win_year) * pct d = today - relativedelta(months=59) - metrics["5Y (ann.) %"] = _stats.cagr(df[df.index >= d], 0.0, compounded) * pct + metrics["5Y (ann.) %"] = _stats.cagr(df[df.index >= d], 0.0, compounded, win_year) * pct d = today - relativedelta(years=10) - metrics["10Y (ann.) %"] = _stats.cagr(df[df.index >= d], 0.0, compounded) * pct + metrics["10Y (ann.) %"] = _stats.cagr(df[df.index >= d], 0.0, compounded, win_year) * pct - metrics["All-time (ann.) %"] = _stats.cagr(df, 0.0, compounded) * pct + metrics["All-time (ann.) %"] = _stats.cagr(df, 0.0, compounded, win_year) * pct # best/worst if mode.lower() == "full": diff --git a/quantstats/stats.py b/quantstats/stats.py index e544f373..228b661b 100644 --- a/quantstats/stats.py +++ b/quantstats/stats.py @@ -537,7 +537,7 @@ def cagr(returns, rf=0.0, compounded=True, periods=252): return res -def rar(returns, rf=0.0): +def rar(returns, rf=0.0, periods=252): """ Calculates the risk-adjusted return of access returns (CAGR / exposure. takes time into account.) @@ -546,7 +546,7 @@ def rar(returns, rf=0.0): In this case, rf is assumed to be expressed in yearly (annualized) terms """ returns = _utils._prepare_returns(returns, rf) - return cagr(returns) / exposure(returns) + return cagr(returns=returns, periods=periods) / exposure(returns) def skew(returns, prepare_returns=True): @@ -569,11 +569,11 @@ def kurtosis(returns, prepare_returns=True): return returns.kurtosis() -def calmar(returns, prepare_returns=True): +def calmar(returns, prepare_returns=True, periods=252): """Calculates the calmar ratio (CAGR% / MaxDD%)""" if prepare_returns: returns = _utils._prepare_returns(returns) - cagr_ratio = cagr(returns) + cagr_ratio = cagr(returns=returns, periods=periods) max_dd = max_drawdown(returns) return cagr_ratio / abs(max_dd) From 9a2734f3021e0d3d865281233f46b56419b00950 Mon Sep 17 00:00:00 2001 From: raotushar Date: Wed, 24 Jan 2024 15:45:28 +0530 Subject: [PATCH 2/3] Add docstrings to stats.py functions --- quantstats/reports.py | 104 +++++++++++++++++++++++++++++++----------- quantstats/stats.py | 81 +++++++++++++++++++++++++++++--- 2 files changed, 153 insertions(+), 32 deletions(-) diff --git a/quantstats/reports.py b/quantstats/reports.py index 129676ab..9b48d7ac 100644 --- a/quantstats/reports.py +++ b/quantstats/reports.py @@ -36,11 +36,13 @@ def _get_trading_periods(periods_per_year=252): + """returns trading periods per year and half year""" half_year = _ceil(periods_per_year / 2) return periods_per_year, half_year def _match_dates(returns, benchmark): + """match dates of returns and benchmark""" if isinstance(returns, _pd.DataFrame): loc = max(returns[returns.columns[0]].ne(0).idxmax(), benchmark.ne(0).idxmax()) else: @@ -66,6 +68,7 @@ def html( match_dates=True, **kwargs, ): + """generates full HTML tear sheet report""" if output is None and not _utils._in_notebook(): raise ValueError("`output` must be specified") @@ -86,9 +89,10 @@ def html( returns = _utils._prepare_returns(returns) strategy_title = kwargs.get("strategy_title", "Strategy") - if isinstance(returns, _pd.DataFrame): - if len(returns.columns) > 1 and isinstance(strategy_title, str): - strategy_title = list(returns.columns) + if isinstance(returns, _pd.DataFrame) and \ + len(returns.columns) > 1 and \ + isinstance(strategy_title, str): + strategy_title = list(returns.columns) if benchmark is not None: benchmark_title = kwargs.get("benchmark_title", "Benchmark") @@ -501,6 +505,7 @@ def full( match_dates=True, **kwargs, ): + """calculates and plots full performance metrics""" # prepare timeseries if match_dates: @@ -517,9 +522,10 @@ def full( strategy_title = kwargs.get("strategy_title", "Strategy") active = kwargs.get("active_returns", "False") - if isinstance(returns, _pd.DataFrame): - if len(returns.columns) > 1 and isinstance(strategy_title, str): - strategy_title = list(returns.columns) + if isinstance(returns, _pd.DataFrame) and \ + len(returns.columns) > 1 and \ + isinstance(strategy_title, str): + strategy_title = list(returns.columns) if benchmark is not None: benchmark.name = benchmark_title @@ -651,6 +657,7 @@ def basic( match_dates=True, **kwargs, ): + """calculates and plots basic performance metrics""" # prepare timeseries if match_dates: @@ -667,9 +674,10 @@ def basic( strategy_title = kwargs.get("strategy_title", "Strategy") active = kwargs.get("active_returns", "False") - if isinstance(returns, _pd.DataFrame): - if len(returns.columns) > 1 and isinstance(strategy_title, str): - strategy_title = list(returns.columns) + if isinstance(returns, _pd.DataFrame) and \ + len(returns.columns) > 1 and \ + isinstance(strategy_title, str): + strategy_title = list(returns.columns) if _utils._in_notebook(): iDisplay(iHTML("

Performance Metrics

")) @@ -731,6 +739,7 @@ def metrics( match_dates=True, **kwargs, ): + """calculates and displays various performance metrics""" if match_dates: returns = returns.dropna() @@ -930,13 +939,21 @@ def metrics( metrics["~~~~~~~~~~"] = blank metrics["Expected Daily %%"] = ( - _stats.expected_return(df, compounded=compounded, prepare_returns=False) * pct + _stats.expected_return(df, + compounded=compounded, + prepare_returns=False) * pct ) metrics["Expected Monthly %%"] = ( - _stats.expected_return(df, compounded=compounded, aggregate="M", prepare_returns=False) * pct + _stats.expected_return(df, + compounded=compounded, + aggregate="M", + prepare_returns=False) * pct ) metrics["Expected Yearly %%"] = ( - _stats.expected_return(df, compounded=compounded, aggregate="A", prepare_returns=False) * pct + _stats.expected_return(df, + compounded=compounded, + aggregate="A", + prepare_returns=False) * pct ) metrics["Kelly Criterion %"] = ( _stats.kelly_criterion(df, prepare_returns=False) * pct @@ -991,32 +1008,52 @@ def metrics( metrics["1Y %"] = comp_func(df[df.index >= d]) * pct d = today - relativedelta(months=35) - metrics["3Y (ann.) %"] = _stats.cagr(df[df.index >= d], 0.0, compounded, win_year) * pct + metrics["3Y (ann.) %"] = _stats.cagr(df[df.index >= d], + 0.0, + compounded, + win_year) * pct d = today - relativedelta(months=59) - metrics["5Y (ann.) %"] = _stats.cagr(df[df.index >= d], 0.0, compounded, win_year) * pct + metrics["5Y (ann.) %"] = _stats.cagr(df[df.index >= d], + 0.0, + compounded, + win_year) * pct d = today - relativedelta(years=10) - metrics["10Y (ann.) %"] = _stats.cagr(df[df.index >= d], 0.0, compounded, win_year) * pct + metrics["10Y (ann.) %"] = _stats.cagr(df[df.index >= d], + 0.0, + compounded, + win_year) * pct metrics["All-time (ann.) %"] = _stats.cagr(df, 0.0, compounded, win_year) * pct # best/worst if mode.lower() == "full": metrics["~~~"] = blank - metrics["Best Day %"] = _stats.best(df, compounded=compounded, prepare_returns=False) * pct + metrics["Best Day %"] = _stats.best(df, + compounded=compounded, + prepare_returns=False) * pct metrics["Worst Day %"] = _stats.worst(df, prepare_returns=False) * pct metrics["Best Month %"] = ( - _stats.best(df, compounded=compounded, aggregate="M", prepare_returns=False) * pct + _stats.best(df, + compounded=compounded, + aggregate="M", + prepare_returns=False) * pct ) metrics["Worst Month %"] = ( _stats.worst(df, aggregate="M", prepare_returns=False) * pct ) metrics["Best Year %"] = ( - _stats.best(df, compounded=compounded, aggregate="A", prepare_returns=False) * pct + _stats.best(df, + compounded=compounded, + aggregate="A", + prepare_returns=False) * pct ) metrics["Worst Year %"] = ( - _stats.worst(df, compounded=compounded, aggregate="A", prepare_returns=False) * pct + _stats.worst(df, + compounded=compounded, + aggregate="A", + prepare_returns=False) * pct ) # dd @@ -1031,14 +1068,23 @@ def metrics( if mode.lower() == "full": metrics["~~~~~"] = blank metrics["Avg. Up Month %"] = ( - _stats.avg_win(df, compounded=compounded, aggregate="M", prepare_returns=False) * pct + _stats.avg_win(df, + compounded=compounded, + aggregate="M", + prepare_returns=False) * pct ) metrics["Avg. Down Month %"] = ( - _stats.avg_loss(df, compounded=compounded, aggregate="M", prepare_returns=False) * pct + _stats.avg_loss(df, + compounded=compounded, + aggregate="M", + prepare_returns=False) * pct ) metrics["Win Days %%"] = _stats.win_rate(df, prepare_returns=False) * pct metrics["Win Month %%"] = ( - _stats.win_rate(df, compounded=compounded, aggregate="M", prepare_returns=False) * pct + _stats.win_rate(df, + compounded=compounded, + aggregate="M", + prepare_returns=False) * pct ) metrics["Win Quarter %%"] = ( _stats.win_rate(df, compounded=compounded, aggregate="Q", prepare_returns=False) * pct @@ -1212,15 +1258,16 @@ def plots( match_dates=True, **kwargs, ): + """Plots for strategy performance""" benchmark_colname = kwargs.get("benchmark_title", "Benchmark") strategy_colname = kwargs.get("strategy_title", "Strategy") active = kwargs.get("active", "False") - if isinstance(returns, _pd.DataFrame): - if len(returns.columns) > 1: - if isinstance(strategy_colname, str): - strategy_colname = list(returns.columns) + if isinstance(returns, _pd.DataFrame) and \ + len(returns.columns) > 1 and \ + isinstance(strategy_colname, str): + strategy_colname = list(returns.columns) win_year, win_half_year = _get_trading_periods(periods_per_year) @@ -1471,6 +1518,7 @@ def plots( def _calc_dd(df, display=True, as_pct=False): + """Returns drawdown stats""" dd = _stats.to_drawdown_series(df) dd_info = _stats.drawdown_details(dd) @@ -1555,6 +1603,7 @@ def _calc_dd(df, display=True, as_pct=False): def _html_table(obj, showindex="default"): + """Returns HTML table""" obj = _tabulate( obj, headers="keys", tablefmt="html", floatfmt=".2f", showindex=showindex ) @@ -1569,6 +1618,7 @@ def _html_table(obj, showindex="default"): def _download_html(html, filename="quantstats-tearsheet.html"): + """Downloads HTML report""" jscode = _regex.sub( " +", " ", @@ -1589,6 +1639,7 @@ def _download_html(html, filename="quantstats-tearsheet.html"): def _open_html(html): + """Opens HTML in a new tab""" jscode = _regex.sub( " +", " ", @@ -1604,6 +1655,7 @@ def _open_html(html): def _embed_figure(figfiles, figfmt): + """Embeds the figure bytes in the html output""" if isinstance(figfiles, list): embed_string = "\n" for figfile in figfiles: diff --git a/quantstats/stats.py b/quantstats/stats.py index 228b661b..225929e0 100644 --- a/quantstats/stats.py +++ b/quantstats/stats.py @@ -47,7 +47,13 @@ def comp(returns): def distribution(returns, compounded=True, prepare_returns=True): + """Returns the distribution of returns + Args: + * returns (Series, DataFrame): Input return series + * compounded (bool): Calculate compounded returns? + """ def get_outliers(data): + """Returns outliers""" # https://datascience.stackexchange.com/a/57199 Q1 = data.quantile(0.25) Q3 = data.quantile(0.75) @@ -155,6 +161,7 @@ def exposure(returns, prepare_returns=True): returns = _utils._prepare_returns(returns) def _exposure(ret): + """Returns the market exposure time (returns != 0)""" ex = len(ret[(~_np.isnan(ret)) & (ret != 0)]) / len(ret) return _ceil(ex * 100) / 100 @@ -237,6 +244,12 @@ def volatility(returns, periods=252, annualize=True, prepare_returns=True): def rolling_volatility( returns, rolling_period=126, periods_per_year=252, prepare_returns=True ): + """Calculates the rolling volatility of returns for a period + Args: + * returns (Series, DataFrame): Input return series + * rolling_period (int): Rolling period + * periods_per_year: periods per year + """ if prepare_returns: returns = _utils._prepare_returns(returns, rolling_period) @@ -250,7 +263,6 @@ def implied_volatility(returns, periods=252, annualize=True): return logret.rolling(periods).std() * _np.sqrt(periods) return logret.std() - def autocorr_penalty(returns, prepare_returns=False): """Metric to account for auto correlation""" if prepare_returns: @@ -259,7 +271,6 @@ def autocorr_penalty(returns, prepare_returns=False): if isinstance(returns, _pd.DataFrame): returns = returns[returns.columns[0]] - # returns.to_csv('/Users/ran/Desktop/test.csv') num = len(returns) coef = _np.abs(_np.corrcoef(returns[:-1], returns[1:])[0, 1]) corr = [((num - x) / num) * coef**x for x in range(1, num)] @@ -300,6 +311,13 @@ def sharpe(returns, rf=0.0, periods=252, annualize=True, smart=False): def smart_sharpe(returns, rf=0.0, periods=252, annualize=True): + """Calculates the smart sharpe ratio + Args: + * returns (Series, DataFrame): Input return series + * rf (float): Risk-free rate expressed as a yearly (annualized) return + * periods (int): Freq. of returns (252/365 for daily, 12 for monthly) + * annualize: return annualize sharpe? + """ return sharpe(returns, rf, periods, annualize, True) @@ -311,7 +329,14 @@ def rolling_sharpe( periods_per_year=252, prepare_returns=True, ): - + """ Calculates the rolling sharpe ratio + Args: + * returns (Series, DataFrame): Input return series + * rf (float): Risk-free rate expressed as a yearly (annualized) return + * rolling_period (int): Rolling period + * annualize: return annualize sharpe? + * periods_per_year: periods per year + """ if rf != 0 and rolling_period is None: raise Exception("Must provide periods if rf != 0") @@ -355,12 +380,27 @@ def sortino(returns, rf=0, periods=252, annualize=True, smart=False): def smart_sortino(returns, rf=0, periods=252, annualize=True): + """Calculates the smart sortino ratio + Args: + * returns (Series, DataFrame): Input return series + * rf (float): Risk-free rate expressed as a yearly (annualized) return + * periods (int): Freq. of returns (252/365 for daily, 12 for monthly) + * annualize: return annualize sharpe? + """ return sortino(returns, rf, periods, annualize, True) def rolling_sortino( returns, rf=0, rolling_period=126, annualize=True, periods_per_year=252, **kwargs ): + """ Calculates the rolling sortino ratio + Args: + * returns (Series, DataFrame): Input return series + * rf (float): Risk-free rate expressed as a yearly (annualized) return + * rolling_period (int): Rolling period + * annualize: return annualize sharpe? + * periods_per_year: periods per year + """ if rf != 0 and rolling_period is None: raise Exception("Must provide periods if rf != 0") @@ -393,6 +433,14 @@ def adjusted_sortino(returns, rf=0, periods=252, annualize=True, smart=False): def probabilistic_ratio( series, rf=0.0, base="sharpe", periods=252, annualize=False, smart=False ): + """Calculates the probabilistic sharpe ratio + Args: + * series (Series, DataFrame): Input return series + * rf (float): Risk-free rate expressed as a yearly (annualized) return + * periods (int): Freq. of returns (252/365 for daily, 12 for monthly) + * annualize: return annualize sharpe? + * smart: return smart sharpe ratio + """ if base.lower() == "sharpe": base = sharpe(series, periods=periods, annualize=False, smart=smart) @@ -430,6 +478,14 @@ def probabilistic_ratio( def probabilistic_sharpe_ratio( series, rf=0.0, periods=252, annualize=False, smart=False ): + """Calculates the probabilistic sharpe ratio + Args: + * series (Series, DataFrame): Input return series + * rf (float): Risk-free rate expressed as a yearly (annualized) return + * periods (int): Freq. of returns (252/365 for daily, 12 for monthly) + * annualize: return annualize sharpe? + * smart: return smart sharpe ratio + """ return probabilistic_ratio( series, rf, base="sharpe", periods=periods, annualize=annualize, smart=smart ) @@ -438,6 +494,13 @@ def probabilistic_sharpe_ratio( def probabilistic_sortino_ratio( series, rf=0.0, periods=252, annualize=False, smart=False ): + """ Calculates the probabilistic sortino ratio + Args: + * series (Series, DataFrame): Input return series + * rf (float): Risk-free rate expressed as a yearly (annualized) return + * periods (int): Freq. of returns (252/365 for daily, 12 for monthly) + * annualize: return annualize sharpe? + * smart: return smart sharpe ratio""" return probabilistic_ratio( series, rf, base="sortino", periods=periods, annualize=annualize, smart=smart ) @@ -446,6 +509,14 @@ def probabilistic_sortino_ratio( def probabilistic_adjusted_sortino_ratio( series, rf=0.0, periods=252, annualize=False, smart=False ): + """Calculates the probabilistic adjusted sortino ratio + + Args: + * series (Series, DataFrame): Input return series + * rf (float): Risk-free rate expressed as a yearly (annualized) return + * periods (int): Freq. of returns (252/365 for daily, 12 for monthly) + * annualize: return annualize sharpe? + * smart: return smart sharpe ratio""" return probabilistic_ratio( series, rf, @@ -786,7 +857,7 @@ def drawdown_details(drawdown): for every drawdown period """ - def _drawdown_details(drawdown): + def _drawdown_details(drawdown): # mark no drawdown no_dd = drawdown == 0 @@ -887,7 +958,6 @@ def kelly_criterion(returns, prepare_returns=True): def r_squared(returns, benchmark, prepare_returns=True): """Measures the straight line fit of the equity curve""" - # slope, intercept, r_val, p_val, std_err = _linregress( if prepare_returns: returns = _utils._prepare_returns(returns) _, _, r_val, _, _ = _linregress( @@ -956,7 +1026,6 @@ def rolling_greeks(returns, benchmark, periods=252, prepare_returns=True): alpha = df["returns"].mean() - beta * df["benchmark"].mean() - # alpha = alpha * periods return _pd.DataFrame(index=returns.index, data={"beta": beta, "alpha": alpha}) From 330ac837c099a98dcc5effb3aba1b175a0e415f3 Mon Sep 17 00:00:00 2001 From: raotushar Date: Wed, 24 Jan 2024 16:52:39 +0530 Subject: [PATCH 3/3] Refactor metrics calculation and remove commented code --- quantstats/reports.py | 14 -------------- quantstats/stats.py | 6 +++--- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/quantstats/reports.py b/quantstats/reports.py index 9b48d7ac..0349eaf0 100644 --- a/quantstats/reports.py +++ b/quantstats/reports.py @@ -766,11 +766,6 @@ def metrics( else: blank = [""] - # if isinstance(returns, _pd.DataFrame): - # if len(returns.columns) > 1: - # raise ValueError("`returns` needs to be a Pandas Series or one column DataFrame. multi colums DataFrame was passed") - # returns = returns[returns.columns[0]] - if prepare_returns: df = _utils._prepare_returns(returns) @@ -855,19 +850,14 @@ def metrics( ) if mode.lower() == "full": metrics["Smart Sharpe"] = _stats.smart_sharpe(df, rf, win_year, True) - # metrics['Prob. Smart Sharpe Ratio %'] = _stats.probabilistic_sharpe_ratio(df, rf, win_year, False, True) * pct metrics["Sortino"] = _stats.sortino(df, rf, win_year, True) if mode.lower() == "full": - # metrics['Prob. Sortino Ratio %'] = _stats.probabilistic_sortino_ratio(df, rf, win_year, False) * pct metrics["Smart Sortino"] = _stats.smart_sortino(df, rf, win_year, True) - # metrics['Prob. Smart Sortino Ratio %'] = _stats.probabilistic_sortino_ratio(df, rf, win_year, False, True) * pct metrics["Sortino/√2"] = metrics["Sortino"] / _sqrt(2) if mode.lower() == "full": - # metrics['Prob. Sortino/√2 Ratio %'] = _stats.probabilistic_adjusted_sortino_ratio(df, rf, win_year, False) * pct metrics["Smart Sortino/√2"] = metrics["Smart Sortino"] / _sqrt(2) - # metrics['Prob. Smart Sortino/√2 Ratio %'] = _stats.probabilistic_adjusted_sortino_ratio(df, rf, win_year, False, True) * pct metrics["Omega"] = _stats.omega(df, rf, 0.0, win_year) metrics["~~~~~~~~"] = blank @@ -975,10 +965,6 @@ def metrics( metrics["Gain/Pain Ratio"] = _stats.gain_to_pain_ratio(df, rf) metrics["Gain/Pain (1M)"] = _stats.gain_to_pain_ratio(df, rf, "M") - # if mode.lower() == 'full': - # metrics['GPR (3M)'] = _stats.gain_to_pain_ratio(df, rf, "Q") - # metrics['GPR (6M)'] = _stats.gain_to_pain_ratio(df, rf, "2Q") - # metrics['GPR (1Y)'] = _stats.gain_to_pain_ratio(df, rf, "A") metrics["~~~~~~~"] = blank metrics["Payoff Ratio"] = _stats.payoff_ratio(df, prepare_returns=False) diff --git a/quantstats/stats.py b/quantstats/stats.py index 225929e0..6d6432ed 100644 --- a/quantstats/stats.py +++ b/quantstats/stats.py @@ -329,7 +329,7 @@ def rolling_sharpe( periods_per_year=252, prepare_returns=True, ): - """ Calculates the rolling sharpe ratio + """Calculates the rolling sharpe ratio Args: * returns (Series, DataFrame): Input return series * rf (float): Risk-free rate expressed as a yearly (annualized) return @@ -393,7 +393,7 @@ def smart_sortino(returns, rf=0, periods=252, annualize=True): def rolling_sortino( returns, rf=0, rolling_period=126, annualize=True, periods_per_year=252, **kwargs ): - """ Calculates the rolling sortino ratio + """Calculates the rolling sortino ratio Args: * returns (Series, DataFrame): Input return series * rf (float): Risk-free rate expressed as a yearly (annualized) return @@ -494,7 +494,7 @@ def probabilistic_sharpe_ratio( def probabilistic_sortino_ratio( series, rf=0.0, periods=252, annualize=False, smart=False ): - """ Calculates the probabilistic sortino ratio + """Calculates the probabilistic sortino ratio Args: * series (Series, DataFrame): Input return series * rf (float): Risk-free rate expressed as a yearly (annualized) return