diff --git a/quantstats/__init__.py b/quantstats/__init__.py index 1b17699a..c6c4b32f 100644 --- a/quantstats/__init__.py +++ b/quantstats/__init__.py @@ -25,7 +25,7 @@ from . import stats, utils, plots, reports -__all__ = ['stats', 'plots', 'reports', 'utils', 'extend_pandas'] +__all__ = ["stats", "plots", "reports", "utils", "extend_pandas"] # try automatic matplotlib inline utils._in_notebook(matplotlib_inline=True) @@ -102,7 +102,9 @@ def extend_pandas(): _po.treynor_ratio = stats.treynor_ratio _po.probabilistic_sharpe_ratio = stats.probabilistic_sharpe_ratio _po.probabilistic_sortino_ratio = stats.probabilistic_sortino_ratio - _po.probabilistic_adjusted_sortino_ratio = stats.probabilistic_adjusted_sortino_ratio + _po.probabilistic_adjusted_sortino_ratio = ( + stats.probabilistic_adjusted_sortino_ratio + ) # methods from utils _po.to_returns = utils.to_returns @@ -146,4 +148,6 @@ def extend_pandas(): _po.plot_monthly_heatmap = plots.monthly_heatmap _po.metrics = reports.metrics + + # extend_pandas() diff --git a/quantstats/_plotting/core.py b/quantstats/_plotting/core.py index ef256a77..4a29f499 100644 --- a/quantstats/_plotting/core.py +++ b/quantstats/_plotting/core.py @@ -19,6 +19,7 @@ # limitations under the License. import matplotlib.pyplot as _plt + try: _plt.rcParams["font.family"] = "Arial" except Exception: @@ -27,57 +28,93 @@ import matplotlib.dates as _mdates from matplotlib.ticker import ( FormatStrFormatter as _FormatStrFormatter, - FuncFormatter as _FuncFormatter + FuncFormatter as _FuncFormatter, ) import pandas as _pd import numpy as _np import seaborn as _sns from .. import ( - stats as _stats, utils as _utils, + stats as _stats, + utils as _utils, ) -_sns.set(font_scale=1.1, rc={ - 'figure.figsize': (10, 6), - 'axes.facecolor': 'white', - 'figure.facecolor': 'white', - 'grid.color': '#dddddd', - 'grid.linewidth': 0.5, - "lines.linewidth": 1.5, - 'text.color': '#333333', - 'xtick.color': '#666666', - 'ytick.color': '#666666' -}) +_sns.set( + font_scale=1.1, + rc={ + "figure.figsize": (10, 6), + "axes.facecolor": "white", + "figure.facecolor": "white", + "grid.color": "#dddddd", + "grid.linewidth": 0.5, + "lines.linewidth": 1.5, + "text.color": "#333333", + "xtick.color": "#666666", + "ytick.color": "#666666", + }, +) -_FLATUI_COLORS = ['#FEDD78', '#348DC1', '#BA516B', '#4FA487', '#9B59B6', - '#613F66', '#84B082', '#DC136C', '#559CAD', '#4A5899'] -_GRAYSCALE_COLORS = ['#000000', '#222222', '#555555', '#888888', '#AAAAAA', - '#CCCCCC', '#EEEEEE', '#333333', '#666666', '#999999'] +_FLATUI_COLORS = [ + "#FEDD78", + "#348DC1", + "#BA516B", + "#4FA487", + "#9B59B6", + "#613F66", + "#84B082", + "#DC136C", + "#559CAD", + "#4A5899", +] +_GRAYSCALE_COLORS = [ + "#000000", + "#222222", + "#555555", + "#888888", + "#AAAAAA", + "#CCCCCC", + "#EEEEEE", + "#333333", + "#666666", + "#999999", +] def _get_colors(grayscale): colors = _FLATUI_COLORS - ls = '-' - alpha = .8 + ls = "-" + alpha = 0.8 if grayscale: colors = _GRAYSCALE_COLORS - ls = '-' + ls = "-" alpha = 0.5 return colors, ls, alpha -def plot_returns_bars(returns, benchmark=None, - returns_label="Strategy", - hline=None, hlw=None, hlcolor="red", hllabel="", - resample="A", title="Returns", match_volatility=False, - log_scale=False, figsize=(10, 6), - grayscale=False, fontname='Arial', ylabel=True, - subtitle=True, savefig=None, show=True): +def plot_returns_bars( + returns, + benchmark=None, + returns_label="Strategy", + hline=None, + hlw=None, + hlcolor="red", + hllabel="", + resample="A", + title="Returns", + match_volatility=False, + log_scale=False, + figsize=(10, 6), + grayscale=False, + fontname="Arial", + ylabel=True, + subtitle=True, + savefig=None, + show=True, +): if match_volatility and benchmark is None: - raise ValueError('match_volatility requires passing of ' - 'benchmark.') + raise ValueError("match_volatility requires passing of " "benchmark.") if match_volatility and benchmark is not None: bmark_vol = benchmark.loc[returns.index].std() returns = (returns / returns.std()) * bmark_vol @@ -87,7 +124,9 @@ def plot_returns_bars(returns, benchmark=None, if isinstance(returns, _pd.Series): df = _pd.DataFrame(index=returns.index, data={returns.name: returns}) elif isinstance(returns, _pd.DataFrame): - df = _pd.DataFrame(index=returns.index, data={col: returns[col] for col in returns.columns}) + df = _pd.DataFrame( + index=returns.index, data={col: returns[col] for col in returns.columns} + ) if isinstance(benchmark, _pd.Series): df[benchmark.name] = benchmark[benchmark.index.isin(returns.index)] if isinstance(returns, _pd.Series): @@ -98,32 +137,37 @@ def plot_returns_bars(returns, benchmark=None, df = df.dropna() if resample is not None: - df = df.resample(resample).apply( - _stats.comp).resample(resample).last() + df = df.resample(resample).apply(_stats.comp).resample(resample).last() # --------------- fig, ax = _plt.subplots(figsize=figsize) - ax.spines['top'].set_visible(False) - ax.spines['right'].set_visible(False) - ax.spines['bottom'].set_visible(False) - ax.spines['left'].set_visible(False) + ax.spines["top"].set_visible(False) + ax.spines["right"].set_visible(False) + ax.spines["bottom"].set_visible(False) + ax.spines["left"].set_visible(False) # use a more precise date string for the x axis locations in the toolbar - fig.suptitle(title, y=.94, fontweight="bold", fontname=fontname, - fontsize=14, color="black") + fig.suptitle( + title, y=0.94, fontweight="bold", fontname=fontname, fontsize=14, color="black" + ) if subtitle: - ax.set_title("%s - %s \n" % ( - df.index.date[:1][0].strftime('%Y'), - df.index.date[-1:][0].strftime('%Y') - ), fontsize=12, color='gray') + ax.set_title( + "%s - %s \n" + % ( + df.index.date[:1][0].strftime("%Y"), + df.index.date[-1:][0].strftime("%Y"), + ), + fontsize=12, + color="gray", + ) if benchmark is None: colors = colors[1:] - df.plot(kind='bar', ax=ax, color=colors) + df.plot(kind="bar", ax=ax, color=colors) - fig.set_facecolor('white') - ax.set_facecolor('white') + fig.set_facecolor("white") + ax.set_facecolor("white") try: ax.set_xticklabels(df.index.year) @@ -135,9 +179,11 @@ def plot_returns_bars(returns, benchmark=None, # ax.fmt_xdata = _mdates.DateFormatter('%Y-%m-%d') # years = sorted(list(set(df.index.year))) if len(years) > 10: - mod = int(len(years)/10) - _plt.xticks(_np.arange(len(years)), [ - str(year) if not i % mod else '' for i, year in enumerate(years)]) + mod = int(len(years) / 10) + _plt.xticks( + _np.arange(len(years)), + [str(year) if not i % mod else "" for i, year in enumerate(years)], + ) # rotate and align the tick labels so they look better fig.autofmt_xdate() @@ -145,9 +191,8 @@ def plot_returns_bars(returns, benchmark=None, if hline is not None: if not isinstance(hline, _pd.Series): if grayscale: - hlcolor = 'gray' - ax.axhline(hline, ls="--", lw=hlw, color=hlcolor, - label=hllabel, zorder=2) + hlcolor = "gray" + ax.axhline(hline, ls="--", lw=hlw, color=hlcolor, label=hllabel, zorder=2) ax.axhline(0, ls="--", lw=1, color="#000000", zorder=2) @@ -156,11 +201,12 @@ def plot_returns_bars(returns, benchmark=None, _plt.yscale("symlog" if log_scale else "linear") - ax.set_xlabel('') + ax.set_xlabel("") if ylabel: - ax.set_ylabel("Returns", fontname=fontname, - fontweight='bold', fontsize=12, color="black") - ax.yaxis.set_label_coords(-.1, .5) + ax.set_ylabel( + "Returns", fontname=fontname, fontweight="bold", fontsize=12, color="black" + ) + ax.yaxis.set_label_coords(-0.1, 0.5) ax.yaxis.set_major_formatter(_FuncFormatter(format_pct_axis)) @@ -194,14 +240,31 @@ def plot_returns_bars(returns, benchmark=None, return None -def plot_timeseries(returns, benchmark=None, - title="Returns", compound=False, cumulative=True, - fill=False, returns_label="Strategy", - hline=None, hlw=None, hlcolor="red", hllabel="", - percent=True, match_volatility=False, log_scale=False, - resample=None, lw=1.5, figsize=(10, 6), ylabel="", - grayscale=False, fontname="Arial", - subtitle=True, savefig=None, show=True): +def plot_timeseries( + returns, + benchmark=None, + title="Returns", + compound=False, + cumulative=True, + fill=False, + returns_label="Strategy", + hline=None, + hlw=None, + hlcolor="red", + hllabel="", + percent=True, + match_volatility=False, + log_scale=False, + resample=None, + lw=1.5, + figsize=(10, 6), + ylabel="", + grayscale=False, + fontname="Arial", + subtitle=True, + savefig=None, + show=True, +): colors, ls, alpha = _get_colors(grayscale) @@ -210,8 +273,7 @@ def plot_timeseries(returns, benchmark=None, benchmark.fillna(0, inplace=True) if match_volatility and benchmark is None: - raise ValueError('match_volatility requires passing of ' - 'benchmark.') + raise ValueError("match_volatility requires passing of " "benchmark.") if match_volatility and benchmark is not None: bmark_vol = benchmark.std() returns = (returns / returns.std()) * bmark_vol @@ -232,45 +294,52 @@ def plot_timeseries(returns, benchmark=None, returns = returns.last() if compound is True else returns.sum() if isinstance(benchmark, _pd.Series): benchmark = benchmark.resample(resample) - benchmark = benchmark.last( - ) if compound is True else benchmark.sum() + benchmark = benchmark.last() if compound is True else benchmark.sum() # --------------- fig, ax = _plt.subplots(figsize=figsize) - ax.spines['top'].set_visible(False) - ax.spines['right'].set_visible(False) - ax.spines['bottom'].set_visible(False) - ax.spines['left'].set_visible(False) + ax.spines["top"].set_visible(False) + ax.spines["right"].set_visible(False) + ax.spines["bottom"].set_visible(False) + ax.spines["left"].set_visible(False) - fig.suptitle(title, y=.94, fontweight="bold", fontname=fontname, - fontsize=14, color="black") + fig.suptitle( + title, y=0.94, fontweight="bold", fontname=fontname, fontsize=14, color="black" + ) if subtitle: - ax.set_title("%s - %s \n" % ( - returns.index.date[:1][0].strftime('%e %b \'%y'), - returns.index.date[-1:][0].strftime('%e %b \'%y') - ), fontsize=12, color='gray') - - fig.set_facecolor('white') - ax.set_facecolor('white') + ax.set_title( + "%s - %s \n" + % ( + returns.index.date[:1][0].strftime("%e %b '%y"), + returns.index.date[-1:][0].strftime("%e %b '%y"), + ), + fontsize=12, + color="gray", + ) + + fig.set_facecolor("white") + ax.set_facecolor("white") if isinstance(benchmark, _pd.Series): ax.plot(benchmark, lw=lw, ls=ls, label=benchmark.name, color=colors[0]) - alpha = .25 if grayscale else 1 + alpha = 0.25 if grayscale else 1 if isinstance(returns, _pd.Series): ax.plot(returns, lw=lw, label=returns.name, color=colors[1], alpha=alpha) elif isinstance(returns, _pd.DataFrame): # color_dict = {col: colors[i+1] for i, col in enumerate(returns.columns)} for i, col in enumerate(returns.columns): - ax.plot(returns[col], lw=lw, label=col, alpha=alpha, color=colors[i+1]) + ax.plot(returns[col], lw=lw, label=col, alpha=alpha, color=colors[i + 1]) if fill: if isinstance(returns, _pd.Series): - ax.fill_between(returns.index, 0, returns, color=colors[1], alpha=.25) + ax.fill_between(returns.index, 0, returns, color=colors[1], alpha=0.25) elif isinstance(returns, _pd.DataFrame): for i, col in enumerate(returns.columns): - ax.fill_between(returns[col].index, 0, returns[col], color=colors[i+1], alpha=.25) + ax.fill_between( + returns[col].index, 0, returns[col], color=colors[i + 1], alpha=0.25 + ) # rotate and align the tick labels so they look better fig.autofmt_xdate() @@ -281,14 +350,11 @@ def plot_timeseries(returns, benchmark=None, if hline is not None: if not isinstance(hline, _pd.Series): if grayscale: - hlcolor = 'black' - ax.axhline(hline, ls="--", lw=hlw, color=hlcolor, - label=hllabel, zorder=2) + hlcolor = "black" + ax.axhline(hline, ls="--", lw=hlw, color=hlcolor, label=hllabel, zorder=2) - ax.axhline(0, ls="-", lw=1, - color='gray', zorder=1) - ax.axhline(0, ls="--", lw=1, - color='white' if grayscale else 'black', zorder=2) + ax.axhline(0, ls="-", lw=1, color="gray", zorder=1) + ax.axhline(0, ls="--", lw=1, color="white" if grayscale else "black", zorder=2) # if isinstance(benchmark, _pd.Series) or hline is not None: ax.legend(fontsize=11) @@ -300,11 +366,12 @@ def plot_timeseries(returns, benchmark=None, # ax.yaxis.set_major_formatter(_plt.FuncFormatter( # lambda x, loc: "{:,}%".format(int(x*100)))) - ax.set_xlabel('') + ax.set_xlabel("") if ylabel: - ax.set_ylabel(ylabel, fontname=fontname, - fontweight='bold', fontsize=12, color="black") - ax.yaxis.set_label_coords(-.1, .5) + ax.set_ylabel( + ylabel, fontname=fontname, fontweight="bold", fontsize=12, color="black" + ) + ax.yaxis.set_label_coords(-0.1, 0.5) if benchmark is None and len(_pd.DataFrame(returns).columns) == 1: ax.get_legend().remove() @@ -336,11 +403,22 @@ def plot_timeseries(returns, benchmark=None, return None -def plot_histogram(returns, benchmark, resample="M", bins=20, - fontname='Arial', grayscale=False, - title="Returns", kde=True, figsize=(10, 6), - ylabel=True, subtitle=True, compounded=True, - savefig=None, show=True): +def plot_histogram( + returns, + benchmark, + resample="M", + bins=20, + fontname="Arial", + grayscale=False, + title="Returns", + kde=True, + figsize=(10, 6), + ylabel=True, + subtitle=True, + compounded=True, + savefig=None, + show=True, +): # colors = ['#348dc1', '#003366', 'red'] # if grayscale: @@ -350,31 +428,43 @@ def plot_histogram(returns, benchmark, resample="M", bins=20, apply_fnc = _stats.comp if compounded else _np.sum if benchmark is not None: - benchmark = benchmark.fillna(0).resample(resample).apply( - apply_fnc).resample(resample).last() - - returns = returns.fillna(0).resample(resample).apply( - apply_fnc).resample(resample).last() + benchmark = ( + benchmark.fillna(0) + .resample(resample) + .apply(apply_fnc) + .resample(resample) + .last() + ) + + returns = ( + returns.fillna(0).resample(resample).apply(apply_fnc).resample(resample).last() + ) figsize = (0.995 * figsize[0], figsize[1]) fig, ax = _plt.subplots(figsize=figsize) - ax.spines['top'].set_visible(False) - ax.spines['right'].set_visible(False) - ax.spines['bottom'].set_visible(False) - ax.spines['left'].set_visible(False) + ax.spines["top"].set_visible(False) + ax.spines["right"].set_visible(False) + ax.spines["bottom"].set_visible(False) + ax.spines["left"].set_visible(False) - fig.suptitle(title, y=.94, fontweight="bold", fontname=fontname, - fontsize=14, color="black") + fig.suptitle( + title, y=0.94, fontweight="bold", fontname=fontname, fontsize=14, color="black" + ) if subtitle: - ax.set_title("%s - %s \n" % ( - returns.index.date[:1][0].strftime('%Y'), - returns.index.date[-1:][0].strftime('%Y') - ), fontsize=12, color='gray') - - fig.set_facecolor('white') - ax.set_facecolor('white') + ax.set_title( + "%s - %s \n" + % ( + returns.index.date[:1][0].strftime("%Y"), + returns.index.date[-1:][0].strftime("%Y"), + ), + fontsize=12, + color="gray", + ) + + fig.set_facecolor("white") + ax.set_facecolor("white") if isinstance(returns, _pd.DataFrame) and len(returns.columns) == 1: returns = returns[returns.columns[0]] @@ -382,65 +472,102 @@ def plot_histogram(returns, benchmark, resample="M", bins=20, pallete = colors[1:2] if benchmark is None else colors[:2] alpha = 0.7 if isinstance(returns, _pd.DataFrame): - pallete = colors[1:len(returns.columns)+1] if benchmark is None else colors[:len(returns.columns)+1] + pallete = ( + colors[1 : len(returns.columns) + 1] + if benchmark is None + else colors[: len(returns.columns) + 1] + ) if len(returns.columns) > 1: alpha = 0.5 if benchmark is not None: if isinstance(returns, _pd.Series): - combined_returns = benchmark.to_frame().join(returns.to_frame()) \ - .stack().reset_index() \ - .rename(columns={'level_1': '', 0: 'Returns'}) + combined_returns = ( + benchmark.to_frame() + .join(returns.to_frame()) + .stack() + .reset_index() + .rename(columns={"level_1": "", 0: "Returns"}) + ) elif isinstance(returns, _pd.DataFrame): - combined_returns = benchmark.to_frame().join(returns) \ - .stack().reset_index() \ - .rename(columns={'level_1': '', 0: 'Returns'}) - x = _sns.histplot(data=combined_returns, x='Returns', - bins=bins, alpha=alpha, kde=kde, - stat="density", hue='', - palette=pallete, - ax=ax) + combined_returns = ( + benchmark.to_frame() + .join(returns) + .stack() + .reset_index() + .rename(columns={"level_1": "", 0: "Returns"}) + ) + x = _sns.histplot( + data=combined_returns, + x="Returns", + bins=bins, + alpha=alpha, + kde=kde, + stat="density", + hue="", + palette=pallete, + ax=ax, + ) else: if isinstance(returns, _pd.Series): combined_returns = returns.copy() if kde: - _sns.kdeplot(data=combined_returns, color='black', ax=ax) - x = _sns.histplot(data=combined_returns, bins=bins, - alpha=alpha, - kde=False, - stat="density", - color=colors[1], - ax=ax) + _sns.kdeplot(data=combined_returns, color="black", ax=ax) + x = _sns.histplot( + data=combined_returns, + bins=bins, + alpha=alpha, + kde=False, + stat="density", + color=colors[1], + ax=ax, + ) elif isinstance(returns, _pd.DataFrame): - combined_returns = returns.stack().reset_index() \ - .rename(columns={'level_1': '', 0: 'Returns'}) + combined_returns = ( + returns.stack() + .reset_index() + .rename(columns={"level_1": "", 0: "Returns"}) + ) # _sns.kdeplot(data=combined_returns, color='black', ax=ax) - x = _sns.histplot(data=combined_returns, x='Returns', - bins=bins, alpha=alpha, kde=kde, - stat="density", hue='', - palette=pallete, - ax=ax) + x = _sns.histplot( + data=combined_returns, + x="Returns", + bins=bins, + alpha=alpha, + kde=kde, + stat="density", + hue="", + palette=pallete, + ax=ax, + ) # Why do we need average? if isinstance(combined_returns, _pd.Series) or len(combined_returns.columns) == 1: - ax.axvline(combined_returns.mean(), ls="--", lw=1.5, - zorder=2, label="Average", color="red") - + ax.axvline( + combined_returns.mean(), + ls="--", + lw=1.5, + zorder=2, + label="Average", + color="red", + ) # _plt.setp(x.get_legend().get_texts(), fontsize=11) - ax.xaxis.set_major_formatter(_plt.FuncFormatter( - lambda x, loc: "{:,}%".format(int(x*100)))) + ax.xaxis.set_major_formatter( + _plt.FuncFormatter(lambda x, loc: "{:,}%".format(int(x * 100))) + ) # Removed static lines for clarity # ax.axhline(0.01, lw=1, color="#000000", zorder=2) # ax.axvline(0, lw=1, color="#000000", zorder=2) - ax.set_xlabel('') - ax.set_ylabel("Occurrences", fontname=fontname, - fontweight='bold', fontsize=12, color="black") - ax.yaxis.set_label_coords(-.1, .5) + ax.set_xlabel("") + ax.set_ylabel( + "Occurrences", fontname=fontname, fontweight="bold", fontsize=12, color="black" + ) + ax.yaxis.set_label_coords(-0.1, 0.5) # fig.autofmt_xdate() @@ -471,20 +598,32 @@ def plot_histogram(returns, benchmark, resample="M", bins=20, return None -def plot_rolling_stats(returns, benchmark=None, title="", - returns_label="Strategy", - hline=None, hlw=None, hlcolor="red", hllabel="", - lw=1.5, figsize=(10, 6), ylabel="", - grayscale=False, fontname="Arial", subtitle=True, - savefig=None, show=True): +def plot_rolling_stats( + returns, + benchmark=None, + title="", + returns_label="Strategy", + hline=None, + hlw=None, + hlcolor="red", + hllabel="", + lw=1.5, + figsize=(10, 6), + ylabel="", + grayscale=False, + fontname="Arial", + subtitle=True, + savefig=None, + show=True, +): colors, _, _ = _get_colors(grayscale) fig, ax = _plt.subplots(figsize=figsize) - ax.spines['top'].set_visible(False) - ax.spines['right'].set_visible(False) - ax.spines['bottom'].set_visible(False) - ax.spines['left'].set_visible(False) + ax.spines["top"].set_visible(False) + ax.spines["right"].set_visible(False) + ax.spines["bottom"].set_visible(False) + ax.spines["left"].set_visible(False) if isinstance(returns, _pd.DataFrame): returns_label = list(returns.columns) @@ -492,59 +631,70 @@ def plot_rolling_stats(returns, benchmark=None, title="", if isinstance(returns, _pd.Series): df = _pd.DataFrame(index=returns.index, data={returns_label: returns}) elif isinstance(returns, _pd.DataFrame): - df = _pd.DataFrame(index=returns.index, data={col: returns[col] for col in returns.columns}) + df = _pd.DataFrame( + index=returns.index, data={col: returns[col] for col in returns.columns} + ) if isinstance(benchmark, _pd.Series): - df['Benchmark'] = benchmark[benchmark.index.isin(returns.index)] + df["Benchmark"] = benchmark[benchmark.index.isin(returns.index)] if isinstance(returns, _pd.Series): - df = df[['Benchmark', returns_label]].dropna() - ax.plot(df[returns_label].dropna(), lw=lw, - label=returns.name, color=colors[1]) + df = df[["Benchmark", returns_label]].dropna() + ax.plot( + df[returns_label].dropna(), lw=lw, label=returns.name, color=colors[1] + ) elif isinstance(returns, _pd.DataFrame): - col_names = ['Benchmark', returns_label] + col_names = ["Benchmark", returns_label] df = df[list(_pd.core.common.flatten(col_names))].dropna() for i, col in enumerate(returns_label): - ax.plot(df[col], lw=lw, label=col, color=colors[i+1]) - ax.plot(df['Benchmark'], lw=lw, label=benchmark.name, - color=colors[0], alpha=.8) + ax.plot(df[col], lw=lw, label=col, color=colors[i + 1]) + ax.plot( + df["Benchmark"], lw=lw, label=benchmark.name, color=colors[0], alpha=0.8 + ) else: if isinstance(returns, _pd.Series): df = df[[returns_label]].dropna() - ax.plot(df[returns_label].dropna(), lw=lw, - label=returns.name, color=colors[1]) + ax.plot( + df[returns_label].dropna(), lw=lw, label=returns.name, color=colors[1] + ) elif isinstance(returns, _pd.DataFrame): df = df[returns_label].dropna() for i, col in enumerate(returns_label): - ax.plot(df[col], lw=lw, label=col, color=colors[i+1]) + ax.plot(df[col], lw=lw, label=col, color=colors[i + 1]) # rotate and align the tick labels so they look better fig.autofmt_xdate() # use a more precise date string for the x axis locations in the toolbar # ax.fmt_xdata = _mdates.DateFormatter('%Y-%m-%d')\ - fig.suptitle(title, y=.94, fontweight="bold", fontname=fontname, - fontsize=14, color="black") + fig.suptitle( + title, y=0.94, fontweight="bold", fontname=fontname, fontsize=14, color="black" + ) if subtitle: - ax.set_title("%s - %s \n" % ( - df.index.date[:1][0].strftime('%e %b \'%y'), - df.index.date[-1:][0].strftime('%e %b \'%y') - ), fontsize=12, color='gray') + ax.set_title( + "%s - %s \n" + % ( + df.index.date[:1][0].strftime("%e %b '%y"), + df.index.date[-1:][0].strftime("%e %b '%y"), + ), + fontsize=12, + color="gray", + ) if hline is not None: if not isinstance(hline, _pd.Series): if grayscale: - hlcolor = 'black' - ax.axhline(hline, ls="--", lw=hlw, color=hlcolor, - label=hllabel, zorder=2) + hlcolor = "black" + ax.axhline(hline, ls="--", lw=hlw, color=hlcolor, label=hllabel, zorder=2) ax.axhline(0, ls="--", lw=1, color="#000000", zorder=2) if ylabel: - ax.set_ylabel(ylabel, fontname=fontname, - fontweight='bold', fontsize=12, color="black") - ax.yaxis.set_label_coords(-.1, .5) + ax.set_ylabel( + ylabel, fontname=fontname, fontweight="bold", fontsize=12, color="black" + ) + ax.yaxis.set_label_coords(-0.1, 0.5) - ax.yaxis.set_major_formatter(_FormatStrFormatter('%.2f')) + ax.yaxis.set_major_formatter(_FormatStrFormatter("%.2f")) ax.legend(fontsize=11) @@ -577,37 +727,59 @@ def plot_rolling_stats(returns, benchmark=None, title="", return None -def plot_rolling_beta(returns, benchmark, - window1=126, window1_label="", - window2=None, window2_label="", - title="", hlcolor="red", figsize=(10, 6), - grayscale=False, fontname="Arial", lw=1.5, - ylabel=True, subtitle=True, savefig=None, show=True): +def plot_rolling_beta( + returns, + benchmark, + window1=126, + window1_label="", + window2=None, + window2_label="", + title="", + hlcolor="red", + figsize=(10, 6), + grayscale=False, + fontname="Arial", + lw=1.5, + ylabel=True, + subtitle=True, + savefig=None, + show=True, +): colors, _, _ = _get_colors(grayscale) fig, ax = _plt.subplots(figsize=figsize) - ax.spines['top'].set_visible(False) - ax.spines['right'].set_visible(False) - ax.spines['bottom'].set_visible(False) - ax.spines['left'].set_visible(False) + ax.spines["top"].set_visible(False) + ax.spines["right"].set_visible(False) + ax.spines["bottom"].set_visible(False) + ax.spines["left"].set_visible(False) - fig.suptitle(title, y=.94, fontweight="bold", fontname=fontname, - fontsize=14, color="black") + fig.suptitle( + title, y=0.94, fontweight="bold", fontname=fontname, fontsize=14, color="black" + ) if subtitle: - ax.set_title("%s - %s \n" % ( - returns.index.date[:1][0].strftime('%e %b \'%y'), - returns.index.date[-1:][0].strftime('%e %b \'%y') - ), fontsize=12, color='gray') + ax.set_title( + "%s - %s \n" + % ( + returns.index.date[:1][0].strftime("%e %b '%y"), + returns.index.date[-1:][0].strftime("%e %b '%y"), + ), + fontsize=12, + color="gray", + ) i = 1 if isinstance(returns, _pd.Series): - beta = _stats.rolling_greeks(returns, benchmark, window1)['beta'].fillna(0) + beta = _stats.rolling_greeks(returns, benchmark, window1)["beta"].fillna(0) ax.plot(beta, lw=lw, label=window1_label, color=colors[1]) elif isinstance(returns, _pd.DataFrame): - beta = ({col: _stats.rolling_greeks(returns[col], benchmark, window1)['beta'].fillna(0) - for col in returns.columns}) + beta = { + col: _stats.rolling_greeks(returns[col], benchmark, window1)["beta"].fillna( + 0 + ) + for col in returns.columns + } for name, b in beta.items(): ax.plot(b, lw=lw, label=name + " " + f"({window1_label})", color=colors[i]) i += 1 @@ -616,38 +788,60 @@ def plot_rolling_beta(returns, benchmark, if window2: lw = lw - 0.5 if isinstance(returns, _pd.Series): - ax.plot(_stats.rolling_greeks(returns, benchmark, window2)['beta'], - lw=lw, label=window2_label, color="gray", alpha=0.8) + ax.plot( + _stats.rolling_greeks(returns, benchmark, window2)["beta"], + lw=lw, + label=window2_label, + color="gray", + alpha=0.8, + ) elif isinstance(returns, _pd.DataFrame): - betas_w2 = ({col: _stats.rolling_greeks(returns[col], benchmark, window2)['beta'] - for col in returns.columns}) + betas_w2 = { + col: _stats.rolling_greeks(returns[col], benchmark, window2)["beta"] + for col in returns.columns + } for name, beta_w2 in betas_w2.items(): - ax.plot(beta_w2, lw=lw, ls='--', label=name + " " + f"({window2_label})", alpha=0.5, color=colors[i]) + ax.plot( + beta_w2, + lw=lw, + ls="--", + label=name + " " + f"({window2_label})", + alpha=0.5, + color=colors[i], + ) i += 1 - beta_min = beta.min() if isinstance(returns, _pd.Series) else min([b.min() for b in beta.values()]) - beta_max = beta.max() if isinstance(returns, _pd.Series) else max([b.max() for b in beta.values()]) - mmin = min([-100, int(beta_min*100)]) - mmax = max([100, int(beta_max*100)]) - step = 50 if (mmax-mmin) >= 200 else 100 + beta_min = ( + beta.min() + if isinstance(returns, _pd.Series) + else min([b.min() for b in beta.values()]) + ) + beta_max = ( + beta.max() + if isinstance(returns, _pd.Series) + else max([b.max() for b in beta.values()]) + ) + mmin = min([-100, int(beta_min * 100)]) + mmax = max([100, int(beta_max * 100)]) + step = 50 if (mmax - mmin) >= 200 else 100 ax.set_yticks([x / 100 for x in list(range(mmin, mmax, step))]) if isinstance(returns, _pd.Series): - hlcolor = 'black' if grayscale else hlcolor - ax.axhline(beta.mean(), ls="--", lw=1.5, - color=hlcolor, zorder=2) + hlcolor = "black" if grayscale else hlcolor + ax.axhline(beta.mean(), ls="--", lw=1.5, color=hlcolor, zorder=2) ax.axhline(0, ls="--", lw=1, color="#000000", zorder=2) fig.autofmt_xdate() # use a more precise date string for the x axis locations in the toolbar - ax.fmt_xdata = _mdates.DateFormatter('%Y-%m-%d') + ax.fmt_xdata = _mdates.DateFormatter("%Y-%m-%d") if ylabel: - ax.set_ylabel("Beta", fontname=fontname, - fontweight='bold', fontsize=12, color="black") - ax.yaxis.set_label_coords(-.1, .5) + ax.set_ylabel( + "Beta", fontname=fontname, fontweight="bold", fontsize=12, color="black" + ) + ax.yaxis.set_label_coords(-0.1, 0.5) ax.legend(fontsize=11) if benchmark is None and len(_pd.DataFrame(returns).columns) == 1: @@ -680,58 +874,87 @@ def plot_rolling_beta(returns, benchmark, return None -def plot_longest_drawdowns(returns, periods=5, lw=1.5, - fontname='Arial', grayscale=False, title=None, - log_scale=False, figsize=(10, 6), ylabel=True, - subtitle=True, compounded=True, - savefig=None, show=True): - - colors = ['#348dc1', '#003366', 'red'] +def plot_longest_drawdowns( + returns, + periods=5, + lw=1.5, + fontname="Arial", + grayscale=False, + title=None, + log_scale=False, + figsize=(10, 6), + ylabel=True, + subtitle=True, + compounded=True, + savefig=None, + show=True, +): + + colors = ["#348dc1", "#003366", "red"] if grayscale: - colors = ['#000000'] * 3 + colors = ["#000000"] * 3 dd = _stats.to_drawdown_series(returns.fillna(0)) dddf = _stats.drawdown_details(dd) - longest_dd = dddf.sort_values( - by='days', ascending=False, kind='mergesort')[:periods] + longest_dd = dddf.sort_values(by="days", ascending=False, kind="mergesort")[ + :periods + ] fig, ax = _plt.subplots(figsize=figsize) - ax.spines['top'].set_visible(False) - ax.spines['right'].set_visible(False) - ax.spines['bottom'].set_visible(False) - ax.spines['left'].set_visible(False) - - fig.suptitle(f"{title} - Worst %.0f Drawdown Periods" % - periods, y=.94, fontweight="bold", fontname=fontname, - fontsize=14, color="black") + ax.spines["top"].set_visible(False) + ax.spines["right"].set_visible(False) + ax.spines["bottom"].set_visible(False) + ax.spines["left"].set_visible(False) + + fig.suptitle( + f"{title} - Worst %.0f Drawdown Periods" % periods, + y=0.94, + fontweight="bold", + fontname=fontname, + fontsize=14, + color="black", + ) if subtitle: - ax.set_title("%s - %s \n" % ( - returns.index.date[:1][0].strftime('%e %b \'%y'), - returns.index.date[-1:][0].strftime('%e %b \'%y') - ), fontsize=12, color='gray') - - fig.set_facecolor('white') - ax.set_facecolor('white') + ax.set_title( + "%s - %s \n" + % ( + returns.index.date[:1][0].strftime("%e %b '%y"), + returns.index.date[-1:][0].strftime("%e %b '%y"), + ), + fontsize=12, + color="gray", + ) + + fig.set_facecolor("white") + ax.set_facecolor("white") series = _stats.compsum(returns) if compounded else returns.cumsum() ax.plot(series, lw=lw, label="Backtest", color=colors[0]) - highlight = 'black' if grayscale else 'red' + highlight = "black" if grayscale else "red" for _, row in longest_dd.iterrows(): - ax.axvspan(*_mdates.datestr2num([str(row['start']), str(row['end'])]), - color=highlight, alpha=.1) + ax.axvspan( + *_mdates.datestr2num([str(row["start"]), str(row["end"])]), + color=highlight, + alpha=0.1, + ) # rotate and align the tick labels so they look better fig.autofmt_xdate() # use a more precise date string for the x axis locations in the toolbar - ax.fmt_xdata = _mdates.DateFormatter('%Y-%m-%d') + ax.fmt_xdata = _mdates.DateFormatter("%Y-%m-%d") ax.axhline(0, ls="--", lw=1, color="#000000", zorder=2) _plt.yscale("symlog" if log_scale else "linear") if ylabel: - ax.set_ylabel("Cumulative Returns", fontname=fontname, - fontweight='bold', fontsize=12, color="black") - ax.yaxis.set_label_coords(-.1, .5) + ax.set_ylabel( + "Cumulative Returns", + fontname=fontname, + fontweight="bold", + fontsize=12, + color="black", + ) + ax.yaxis.set_label_coords(-0.1, 0.5) ax.yaxis.set_major_formatter(_FuncFormatter(format_pct_axis)) # ax.yaxis.set_major_formatter(_plt.FuncFormatter( @@ -766,76 +989,90 @@ def plot_longest_drawdowns(returns, periods=5, lw=1.5, return None -def plot_distribution(returns, figsize=(10, 6), - fontname='Arial', grayscale=False, ylabel=True, - subtitle=True, compounded=True, title=None, - savefig=None, show=True): +def plot_distribution( + returns, + figsize=(10, 6), + fontname="Arial", + grayscale=False, + ylabel=True, + subtitle=True, + compounded=True, + title=None, + savefig=None, + show=True, +): colors = _FLATUI_COLORS if grayscale: - colors = ['#f9f9f9', '#dddddd', '#bbbbbb', '#999999', '#808080'] + colors = ["#f9f9f9", "#dddddd", "#bbbbbb", "#999999", "#808080"] # colors, ls, alpha = _get_colors(grayscale) port = _pd.DataFrame(returns.fillna(0)) - port.columns = ['Daily'] + port.columns = ["Daily"] apply_fnc = _stats.comp if compounded else _np.sum - port['Weekly'] = port['Daily'].resample( - 'W-MON').apply(apply_fnc) - port['Weekly'].ffill(inplace=True) + port["Weekly"] = port["Daily"].resample("W-MON").apply(apply_fnc) + port["Weekly"].ffill(inplace=True) - port['Monthly'] = port['Daily'].resample( - 'M').apply(apply_fnc) - port['Monthly'].ffill(inplace=True) + port["Monthly"] = port["Daily"].resample("M").apply(apply_fnc) + port["Monthly"].ffill(inplace=True) - port['Quarterly'] = port['Daily'].resample( - 'Q').apply(apply_fnc) - port['Quarterly'].ffill(inplace=True) + port["Quarterly"] = port["Daily"].resample("Q").apply(apply_fnc) + port["Quarterly"].ffill(inplace=True) - port['Yearly'] = port['Daily'].resample( - 'A').apply(apply_fnc) - port['Yearly'].ffill(inplace=True) + port["Yearly"] = port["Daily"].resample("A").apply(apply_fnc) + port["Yearly"].ffill(inplace=True) fig, ax = _plt.subplots(figsize=figsize) - ax.spines['top'].set_visible(False) - ax.spines['right'].set_visible(False) - ax.spines['bottom'].set_visible(False) - ax.spines['left'].set_visible(False) + ax.spines["top"].set_visible(False) + ax.spines["right"].set_visible(False) + ax.spines["bottom"].set_visible(False) + ax.spines["left"].set_visible(False) if title: title = f"{title} - Return Quantiles" else: title = "Return Quantiles" - fig.suptitle(title, y=.94, - fontweight="bold", fontname=fontname, - fontsize=14, color="black") + fig.suptitle( + title, y=0.94, fontweight="bold", fontname=fontname, fontsize=14, color="black" + ) if subtitle: - ax.set_title("%s - %s \n" % ( - returns.index.date[:1][0].strftime('%e %b \'%y'), - returns.index.date[-1:][0].strftime('%e %b \'%y') - ), fontsize=12, color='gray') - - fig.set_facecolor('white') - ax.set_facecolor('white') - - _sns.boxplot(data=port, ax=ax, - palette={ - 'Daily': colors[0], - 'Weekly': colors[1], - 'Monthly': colors[2], - 'Quarterly': colors[3], - 'Yearly': colors[4] - }) - - ax.yaxis.set_major_formatter(_plt.FuncFormatter( - lambda x, loc: "{:,}%".format(int(x*100)))) + ax.set_title( + "%s - %s \n" + % ( + returns.index.date[:1][0].strftime("%e %b '%y"), + returns.index.date[-1:][0].strftime("%e %b '%y"), + ), + fontsize=12, + color="gray", + ) + + fig.set_facecolor("white") + ax.set_facecolor("white") + + _sns.boxplot( + data=port, + ax=ax, + palette={ + "Daily": colors[0], + "Weekly": colors[1], + "Monthly": colors[2], + "Quarterly": colors[3], + "Yearly": colors[4], + }, + ) + + ax.yaxis.set_major_formatter( + _plt.FuncFormatter(lambda x, loc: "{:,}%".format(int(x * 100))) + ) if ylabel: - ax.set_ylabel('Returns', fontname=fontname, - fontweight='bold', fontsize=12, color="black") - ax.yaxis.set_label_coords(-.1, .5) + ax.set_ylabel( + "Returns", fontname=fontname, fontweight="bold", fontsize=12, color="black" + ) + ax.yaxis.set_label_coords(-0.1, 0.5) fig.autofmt_xdate() @@ -865,17 +1102,22 @@ def plot_distribution(returns, figsize=(10, 6), return None -def plot_table(tbl, columns=None, title="", title_loc="left", - header=True, - colWidths=None, - rowLoc='right', - colLoc='right', - colLabels=None, - edges='horizontal', - orient='horizontal', - figsize=(5.5, 6), - savefig=None, - show=False): +def plot_table( + tbl, + columns=None, + title="", + title_loc="left", + header=True, + colWidths=None, + rowLoc="right", + colLoc="right", + colLabels=None, + edges="horizontal", + orient="horizontal", + figsize=(5.5, 6), + savefig=None, + show=False, +): if columns is not None: try: @@ -887,18 +1129,20 @@ def plot_table(tbl, columns=None, title="", title_loc="left", ax = _plt.subplot(111, frame_on=False) if title != "": - ax.set_title(title, fontweight="bold", - fontsize=14, color="black", loc=title_loc) - - the_table = ax.table(cellText=tbl.values, - colWidths=colWidths, - rowLoc=rowLoc, - colLoc=colLoc, - edges=edges, - colLabels=(tbl.columns if header else colLabels), - loc='center', - zorder=2 - ) + ax.set_title( + title, fontweight="bold", fontsize=14, color="black", loc=title_loc + ) + + the_table = ax.table( + cellText=tbl.values, + colWidths=colWidths, + rowLoc=rowLoc, + colLoc=colLoc, + edges=edges, + colLabels=(tbl.columns if header else colLabels), + loc="center", + zorder=2, + ) the_table.auto_set_font_size(False) the_table.set_fontsize(12) @@ -906,17 +1150,17 @@ def plot_table(tbl, columns=None, title="", title_loc="left", for (row, col), cell in the_table.get_celld().items(): cell.set_height(0.08) - cell.set_text_props(color='black') - cell.set_edgecolor('#dddddd') + cell.set_text_props(color="black") + cell.set_edgecolor("#dddddd") if row == 0 and header: - cell.set_edgecolor('black') - cell.set_facecolor('black') + cell.set_edgecolor("black") + cell.set_facecolor("black") cell.set_linewidth(2) - cell.set_text_props(weight='bold', color='black') + cell.set_text_props(weight="bold", color="black") elif col == 0 and "vertical" in orient: - cell.set_edgecolor('#dddddd') + cell.set_edgecolor("#dddddd") cell.set_linewidth(1) - cell.set_text_props(weight='bold', color='black') + cell.set_text_props(weight="bold", color="black") elif row > 1: cell.set_linewidth(1) @@ -952,34 +1196,34 @@ def plot_table(tbl, columns=None, title="", title_loc="left", def format_cur_axis(x, _): if x >= 1e12: - res = '$%1.1fT' % (x * 1e-12) - return res.replace('.0T', 'T') + res = "$%1.1fT" % (x * 1e-12) + return res.replace(".0T", "T") if x >= 1e9: - res = '$%1.1fB' % (x * 1e-9) - return res.replace('.0B', 'B') + res = "$%1.1fB" % (x * 1e-9) + return res.replace(".0B", "B") if x >= 1e6: - res = '$%1.1fM' % (x * 1e-6) - return res.replace('.0M', 'M') + res = "$%1.1fM" % (x * 1e-6) + return res.replace(".0M", "M") if x >= 1e3: - res = '$%1.0fK' % (x * 1e-3) - return res.replace('.0K', 'K') - res = '$%1.0f' % x - return res.replace('.0', '') + res = "$%1.0fK" % (x * 1e-3) + return res.replace(".0K", "K") + res = "$%1.0f" % x + return res.replace(".0", "") def format_pct_axis(x, _): x *= 100 # lambda x, loc: "{:,}%".format(int(x * 100)) if x >= 1e12: - res = '%1.1fT%%' % (x * 1e-12) - return res.replace('.0T%', 'T%') + res = "%1.1fT%%" % (x * 1e-12) + return res.replace(".0T%", "T%") if x >= 1e9: - res = '%1.1fB%%' % (x * 1e-9) - return res.replace('.0B%', 'B%') + res = "%1.1fB%%" % (x * 1e-9) + return res.replace(".0B%", "B%") if x >= 1e6: - res = '%1.1fM%%' % (x * 1e-6) - return res.replace('.0M%', 'M%') + res = "%1.1fM%%" % (x * 1e-6) + return res.replace(".0M%", "M%") if x >= 1e3: - res = '%1.1fK%%' % (x * 1e-3) - return res.replace('.0K%', 'K%') - res = '%1.0f%%' % x - return res.replace('.0%', '%') + res = "%1.1fK%%" % (x * 1e-3) + return res.replace(".0K%", "K%") + res = "%1.0f%%" % x + return res.replace(".0%", "%") diff --git a/quantstats/_plotting/wrappers.py b/quantstats/_plotting/wrappers.py index 10687ec4..90f02f83 100644 --- a/quantstats/_plotting/wrappers.py +++ b/quantstats/_plotting/wrappers.py @@ -22,7 +22,7 @@ import matplotlib.pyplot as _plt from matplotlib.ticker import ( StrMethodFormatter as _StrMethodFormatter, - FuncFormatter as _FuncFormatter + FuncFormatter as _FuncFormatter, ) import numpy as _np @@ -31,19 +31,20 @@ import seaborn as _sns from .. import ( - stats as _stats, utils as _utils, + stats as _stats, + utils as _utils, ) from . import core as _core -_FLATUI_COLORS = ["#fedd78", "#348dc1", "#af4b64", - "#4fa487", "#9b59b6", "#808080"] -_GRAYSCALE_COLORS = (len(_FLATUI_COLORS) * ['black']) + ['white'] +_FLATUI_COLORS = ["#fedd78", "#348dc1", "#af4b64", "#4fa487", "#9b59b6", "#808080"] +_GRAYSCALE_COLORS = (len(_FLATUI_COLORS) * ["black"]) + ["white"] _HAS_PLOTLY = False try: import plotly + _HAS_PLOTLY = True except ImportError: pass @@ -55,14 +56,23 @@ def to_plotly(fig): with warnings.catch_warnings(): warnings.simplefilter("ignore") fig = plotly.tools.mpl_to_plotly(fig) - return plotly.plotly.iplot(fig, filename='quantstats-plot', - overwrite=True) - - -def snapshot(returns, grayscale=False, figsize=(10, 8), - title='Portfolio Summary', fontname='Arial', lw=1.5, - mode="comp", subtitle=True, savefig=None, show=True, - log_scale=False, **kwargs): + return plotly.plotly.iplot(fig, filename="quantstats-plot", overwrite=True) + + +def snapshot( + returns, + grayscale=False, + figsize=(10, 8), + title="Portfolio Summary", + fontname="Arial", + lw=1.5, + mode="comp", + subtitle=True, + savefig=None, + show=True, + log_scale=False, + **kwargs, +): strategy_colname = kwargs.get("strategy_col", "Strategy") @@ -84,52 +94,79 @@ def snapshot(returns, grayscale=False, figsize=(10, 8), if figsize is None: size = list(_plt.gcf().get_size_inches()) - figsize = (size[0], size[0]*.75) + figsize = (size[0], size[0] * 0.75) - fig, axes = _plt.subplots(3, 1, sharex=True, figsize=figsize, - gridspec_kw={'height_ratios': [3, 1, 1]}) + fig, axes = _plt.subplots( + 3, 1, sharex=True, figsize=figsize, gridspec_kw={"height_ratios": [3, 1, 1]} + ) if multi_column: _plt.figtext( - 0, -.05, + 0, + -0.05, " * When a multi-column DataFrame is passed, the mean of all columns will be used as returns.\n" " To change this behavior, use a pandas Series or pass the column name in the `strategy_col` parameter.", - ha="left", fontsize=11, color='black', alpha=0.6, linespacing=1.5) + ha="left", + fontsize=11, + color="black", + alpha=0.6, + linespacing=1.5, + ) for ax in axes: - ax.spines['top'].set_visible(False) - ax.spines['right'].set_visible(False) - ax.spines['bottom'].set_visible(False) - ax.spines['left'].set_visible(False) + ax.spines["top"].set_visible(False) + ax.spines["right"].set_visible(False) + ax.spines["bottom"].set_visible(False) + ax.spines["left"].set_visible(False) - fig.suptitle(title, fontsize=14, y=.97, - fontname=fontname, fontweight='bold', color='black') + fig.suptitle( + title, fontsize=14, y=0.97, fontname=fontname, fontweight="bold", color="black" + ) - fig.set_facecolor('white') + fig.set_facecolor("white") if subtitle: if isinstance(returns, _pd.Series): - axes[0].set_title("%s - %s ; Sharpe: %.2f \n" % ( - returns.index.date[:1][0].strftime('%e %b \'%y'), - returns.index.date[-1:][0].strftime('%e %b \'%y'), - _stats.sharpe(returns) - ), fontsize=12, color='gray') + axes[0].set_title( + "%s - %s ; Sharpe: %.2f \n" + % ( + returns.index.date[:1][0].strftime("%e %b '%y"), + returns.index.date[-1:][0].strftime("%e %b '%y"), + _stats.sharpe(returns), + ), + fontsize=12, + color="gray", + ) elif isinstance(returns, _pd.DataFrame): - axes[0].set_title("\n%s - %s ; " % ( - returns.index.date[:1][0].strftime('%e %b \'%y'), - returns.index.date[-1:][0].strftime('%e %b \'%y') - ), fontsize=12, color='gray') - - axes[0].set_ylabel('Cumulative Return', fontname=fontname, - fontweight='bold', fontsize=12) + axes[0].set_title( + "\n%s - %s ; " + % ( + returns.index.date[:1][0].strftime("%e %b '%y"), + returns.index.date[-1:][0].strftime("%e %b '%y"), + ), + fontsize=12, + color="gray", + ) + + axes[0].set_ylabel( + "Cumulative Return", fontname=fontname, fontweight="bold", fontsize=12 + ) if isinstance(returns, _pd.Series): - axes[0].plot(_stats.compsum(returns) * 100, color=colors[1], - lw=1 if grayscale else lw, zorder=1) + axes[0].plot( + _stats.compsum(returns) * 100, + color=colors[1], + lw=1 if grayscale else lw, + zorder=1, + ) elif isinstance(returns, _pd.DataFrame): for col in returns.columns: - axes[0].plot(_stats.compsum(returns[col]) * 100, label=col, - lw=1 if grayscale else lw, zorder=1) - axes[0].axhline(0, color='silver', lw=1, zorder=0) + axes[0].plot( + _stats.compsum(returns[col]) * 100, + label=col, + lw=1 if grayscale else lw, + zorder=1, + ) + axes[0].axhline(0, color="silver", lw=1, zorder=0) axes[0].set_yscale("symlog" if log_scale else "linear") # axes[0].legend(fontsize=12) @@ -144,41 +181,47 @@ def snapshot(returns, grayscale=False, figsize=(10, 8), ddmin_ticks = int(_utils._round_to_closest(ddmin_ticks, 5)) # ddmin_ticks = int(_utils._round_to_closest(ddmin, 5)) - axes[1].set_ylabel('Drawdown', fontname=fontname, - fontweight='bold', fontsize=12) + axes[1].set_ylabel("Drawdown", fontname=fontname, fontweight="bold", fontsize=12) axes[1].set_yticks(_np.arange(-ddmin, 0, step=ddmin_ticks)) if isinstance(dd, _pd.Series): axes[1].plot(dd, color=colors[2], lw=1 if grayscale else lw, zorder=1) elif isinstance(dd, _pd.DataFrame): for col in dd.columns: axes[1].plot(dd[col], label=col, lw=1 if grayscale else lw, zorder=1) - axes[1].axhline(0, color='silver', lw=1, zorder=0) + axes[1].axhline(0, color="silver", lw=1, zorder=0) if not grayscale: if isinstance(dd, _pd.Series): - axes[1].fill_between(dd.index, 0, dd, color=colors[2], alpha=.25) + axes[1].fill_between(dd.index, 0, dd, color=colors[2], alpha=0.25) elif isinstance(dd, _pd.DataFrame): for i, col in enumerate(dd.columns): - axes[1].fill_between(dd[col].index, 0, dd[col], color=colors[i + 1], alpha=.25) + axes[1].fill_between( + dd[col].index, 0, dd[col], color=colors[i + 1], alpha=0.25 + ) axes[1].set_yscale("symlog" if log_scale else "linear") # axes[1].legend(fontsize=12) - axes[2].set_ylabel('Daily Return', fontname=fontname, - fontweight='bold', fontsize=12) + axes[2].set_ylabel( + "Daily Return", fontname=fontname, fontweight="bold", fontsize=12 + ) if isinstance(returns, _pd.Series): - axes[2].plot(returns * 100, color=colors[0], label=returns.name, lw=0.5, zorder=1) + axes[2].plot( + returns * 100, color=colors[0], label=returns.name, lw=0.5, zorder=1 + ) elif isinstance(returns, _pd.DataFrame): for i, col in enumerate(returns.columns): - axes[2].plot(returns[col] * 100, color=colors[i], label=col, lw=0.5, zorder=1) - axes[2].axhline(0, color='silver', lw=1, zorder=0) - axes[2].axhline(0, color=colors[-1], linestyle='--', lw=1, zorder=2) + axes[2].plot( + returns[col] * 100, color=colors[i], label=col, lw=0.5, zorder=1 + ) + axes[2].axhline(0, color="silver", lw=1, zorder=0) + axes[2].axhline(0, color=colors[-1], linestyle="--", lw=1, zorder=2) axes[2].set_yscale("symlog" if log_scale else "linear") # axes[2].legend(fontsize=12) retmax = _utils._round_to_closest(returns.max() * 100, 5) retmin = _utils._round_to_closest(returns.min() * 100, 5) - retdiff = (retmax - retmin) + retdiff = retmax - retmin steps = 5 if retdiff > 50: steps = retdiff / 5 @@ -188,9 +231,9 @@ def snapshot(returns, grayscale=False, figsize=(10, 8), axes[2].set_yticks(_np.arange(retmin, retmax, step=steps)) for ax in axes: - ax.set_facecolor('white') - ax.yaxis.set_label_coords(-.1, .5) - ax.yaxis.set_major_formatter(_StrMethodFormatter('{x:,.0f}%')) + ax.set_facecolor("white") + ax.yaxis.set_label_coords(-0.1, 0.5) + ax.yaxis.set_major_formatter(_StrMethodFormatter("{x:,.0f}%")) _plt.subplots_adjust(hspace=0, bottom=0, top=1) fig.autofmt_xdate() @@ -221,58 +264,86 @@ def snapshot(returns, grayscale=False, figsize=(10, 8), return None -def earnings(returns, start_balance=1e5, mode="comp", - grayscale=False, figsize=(10, 6), - title='Portfolio Earnings', - fontname='Arial', lw=1.5, - subtitle=True, savefig=None, show=True): +def earnings( + returns, + start_balance=1e5, + mode="comp", + grayscale=False, + figsize=(10, 6), + title="Portfolio Earnings", + fontname="Arial", + lw=1.5, + subtitle=True, + savefig=None, + show=True, +): colors = _GRAYSCALE_COLORS if grayscale else _FLATUI_COLORS - alpha = .5 if grayscale else .8 + alpha = 0.5 if grayscale else 0.8 returns = _utils.make_portfolio(returns, start_balance, mode) if figsize is None: size = list(_plt.gcf().get_size_inches()) - figsize = (size[0], size[0]*.55) + figsize = (size[0], size[0] * 0.55) fig, ax = _plt.subplots(figsize=figsize) - ax.spines['top'].set_visible(False) - ax.spines['right'].set_visible(False) - ax.spines['bottom'].set_visible(False) - ax.spines['left'].set_visible(False) + ax.spines["top"].set_visible(False) + ax.spines["right"].set_visible(False) + ax.spines["bottom"].set_visible(False) + ax.spines["left"].set_visible(False) - fig.suptitle(title, fontsize=14, y=.995, - fontname=fontname, fontweight='bold', color='black') + fig.suptitle( + title, fontsize=14, y=0.995, fontname=fontname, fontweight="bold", color="black" + ) if subtitle: - ax.set_title("\n%s - %s ; P&L: %s (%s) " % ( - returns.index.date[1:2][0].strftime('%e %b \'%y'), - returns.index.date[-1:][0].strftime('%e %b \'%y'), - _utils._score_str("${:,}".format( - round(returns.values[-1]-returns.values[0], 2))), - _utils._score_str("{:,}%".format( - round((returns.values[-1]/returns.values[0]-1)*100, 2))) - ), fontsize=12, color='gray') + ax.set_title( + "\n%s - %s ; P&L: %s (%s) " + % ( + returns.index.date[1:2][0].strftime("%e %b '%y"), + returns.index.date[-1:][0].strftime("%e %b '%y"), + _utils._score_str( + "${:,}".format(round(returns.values[-1] - returns.values[0], 2)) + ), + _utils._score_str( + "{:,}%".format( + round((returns.values[-1] / returns.values[0] - 1) * 100, 2) + ) + ), + ), + fontsize=12, + color="gray", + ) mx = returns.max() returns_max = returns[returns == mx] ix = returns_max[~_np.isnan(returns_max)].index[0] returns_max = _np.where(returns.index == ix, mx, _np.nan) - ax.plot(returns.index, returns_max, marker='o', lw=0, - alpha=alpha, markersize=12, color=colors[0]) - ax.plot(returns.index, returns, color=colors[1], - lw=1 if grayscale else lw) - - ax.set_ylabel('Value of ${:,.0f}'.format(start_balance), - fontname=fontname, fontweight='bold', fontsize=12) + ax.plot( + returns.index, + returns_max, + marker="o", + lw=0, + alpha=alpha, + markersize=12, + color=colors[0], + ) + ax.plot(returns.index, returns, color=colors[1], lw=1 if grayscale else lw) + + ax.set_ylabel( + "Value of ${:,.0f}".format(start_balance), + fontname=fontname, + fontweight="bold", + fontsize=12, + ) ax.yaxis.set_major_formatter(_FuncFormatter(_core.format_cur_axis)) - ax.yaxis.set_label_coords(-.1, .5) + ax.yaxis.set_label_coords(-0.1, 0.5) - fig.set_facecolor('white') - ax.set_facecolor('white') + fig.set_facecolor("white") + ax.set_facecolor("white") fig.autofmt_xdate() try: @@ -301,93 +372,132 @@ def earnings(returns, start_balance=1e5, mode="comp", return None -def returns(returns, benchmark=None, - grayscale=False, figsize=(10, 6), - fontname='Arial', lw=1.5, - match_volatility=False, compound=True, cumulative=True, - resample=None, ylabel="Cumulative Returns", - subtitle=True, savefig=None, show=True, - prepare_returns=True): - - title = 'Cumulative Returns' if compound else 'Returns' +def returns( + returns, + benchmark=None, + grayscale=False, + figsize=(10, 6), + fontname="Arial", + lw=1.5, + match_volatility=False, + compound=True, + cumulative=True, + resample=None, + ylabel="Cumulative Returns", + subtitle=True, + savefig=None, + show=True, + prepare_returns=True, +): + + title = "Cumulative Returns" if compound else "Returns" if benchmark is not None: if isinstance(benchmark, str): - title += ' vs %s' % benchmark.upper() + title += " vs %s" % benchmark.upper() else: - title += ' vs Benchmark' + title += " vs Benchmark" if match_volatility: - title += ' (Volatility Matched)' + title += " (Volatility Matched)" benchmark = _utils._prepare_benchmark(benchmark, returns.index) if prepare_returns: returns = _utils._prepare_returns(returns) - fig = _core.plot_timeseries(returns, benchmark, title, - ylabel=ylabel, - match_volatility=match_volatility, - log_scale=False, - resample=resample, - compound=compound, - cumulative=cumulative, - lw=lw, - figsize=figsize, - fontname=fontname, - grayscale=grayscale, - subtitle=subtitle, - savefig=savefig, show=show) + fig = _core.plot_timeseries( + returns, + benchmark, + title, + ylabel=ylabel, + match_volatility=match_volatility, + log_scale=False, + resample=resample, + compound=compound, + cumulative=cumulative, + lw=lw, + figsize=figsize, + fontname=fontname, + grayscale=grayscale, + subtitle=subtitle, + savefig=savefig, + show=show, + ) if not show: return fig -def log_returns(returns, benchmark=None, - grayscale=False, figsize=(10, 5), - fontname='Arial', lw=1.5, - match_volatility=False, compound=True, cumulative=True, - resample=None, ylabel="Cumulative Returns", - subtitle=True, savefig=None, show=True, - prepare_returns=True): - - title = 'Cumulative Returns' if compound else 'Returns' +def log_returns( + returns, + benchmark=None, + grayscale=False, + figsize=(10, 5), + fontname="Arial", + lw=1.5, + match_volatility=False, + compound=True, + cumulative=True, + resample=None, + ylabel="Cumulative Returns", + subtitle=True, + savefig=None, + show=True, + prepare_returns=True, +): + + title = "Cumulative Returns" if compound else "Returns" if benchmark is not None: if isinstance(benchmark, str): - title += ' vs %s (Log Scaled' % benchmark.upper() + title += " vs %s (Log Scaled" % benchmark.upper() else: - title += ' vs Benchmark (Log Scaled' + title += " vs Benchmark (Log Scaled" if match_volatility: - title += ', Volatility Matched' + title += ", Volatility Matched" else: - title += ' (Log Scaled' - title += ')' + title += " (Log Scaled" + title += ")" if prepare_returns: returns = _utils._prepare_returns(returns) benchmark = _utils._prepare_benchmark(benchmark, returns.index) - fig = _core.plot_timeseries(returns, benchmark, title, - ylabel=ylabel, - match_volatility=match_volatility, - log_scale=True, - resample=resample, - compound=compound, - cumulative=cumulative, - lw=lw, - figsize=figsize, - fontname=fontname, - grayscale=grayscale, - subtitle=subtitle, - savefig=savefig, show=show) + fig = _core.plot_timeseries( + returns, + benchmark, + title, + ylabel=ylabel, + match_volatility=match_volatility, + log_scale=True, + resample=resample, + compound=compound, + cumulative=cumulative, + lw=lw, + figsize=figsize, + fontname=fontname, + grayscale=grayscale, + subtitle=subtitle, + savefig=savefig, + show=show, + ) if not show: return fig -def daily_returns(returns, benchmark, - grayscale=False, figsize=(10, 4), - fontname='Arial', lw=0.5, - log_scale=False, ylabel="Returns", - subtitle=True, savefig=None, show=True, - prepare_returns=True, active=False): +def daily_returns( + returns, + benchmark, + grayscale=False, + figsize=(10, 4), + fontname="Arial", + lw=0.5, + log_scale=False, + ylabel="Returns", + subtitle=True, + savefig=None, + show=True, + prepare_returns=True, + active=False, +): if prepare_returns: returns = _utils._prepare_returns(returns) @@ -395,297 +505,454 @@ def daily_returns(returns, benchmark, benchmark = _utils._prepare_returns(benchmark) returns = returns - benchmark - plot_title = 'Daily Active Returns' if active else 'Daily Returns' - - fig = _core.plot_timeseries(returns, None, plot_title, - ylabel=ylabel, - match_volatility=False, - log_scale=log_scale, - resample='D', - compound=False, - lw=lw, - figsize=figsize, - fontname=fontname, - grayscale=grayscale, - subtitle=subtitle, - savefig=savefig, show=show) + plot_title = "Daily Active Returns" if active else "Daily Returns" + + fig = _core.plot_timeseries( + returns, + None, + plot_title, + ylabel=ylabel, + match_volatility=False, + log_scale=log_scale, + resample="D", + compound=False, + lw=lw, + figsize=figsize, + fontname=fontname, + grayscale=grayscale, + subtitle=subtitle, + savefig=savefig, + show=show, + ) if not show: return fig -def yearly_returns(returns, benchmark=None, - fontname='Arial', grayscale=False, - hlw=1.5, hlcolor="red", hllabel="", - match_volatility=False, - log_scale=False, figsize=(10, 5), ylabel=True, - subtitle=True, compounded=True, - savefig=None, show=True, - prepare_returns=True): - - title = 'EOY Returns' +def yearly_returns( + returns, + benchmark=None, + fontname="Arial", + grayscale=False, + hlw=1.5, + hlcolor="red", + hllabel="", + match_volatility=False, + log_scale=False, + figsize=(10, 5), + ylabel=True, + subtitle=True, + compounded=True, + savefig=None, + show=True, + prepare_returns=True, +): + + title = "EOY Returns" if benchmark is not None: - title += ' vs Benchmark' - benchmark = _utils._prepare_benchmark( - benchmark, returns.index).resample('A').apply( - _stats.comp).resample('A').last() + title += " vs Benchmark" + benchmark = ( + _utils._prepare_benchmark(benchmark, returns.index) + .resample("A") + .apply(_stats.comp) + .resample("A") + .last() + ) if prepare_returns: returns = _utils._prepare_returns(returns) if compounded: - returns = returns.resample('A').apply(_stats.comp) + returns = returns.resample("A").apply(_stats.comp) else: - returns = returns.resample('A').apply(_df.sum) - returns = returns.resample('A').last() - - fig = _core.plot_returns_bars(returns, benchmark, - fontname=fontname, - hline=returns.mean(), - hlw=hlw, - hllabel=hllabel, - hlcolor=hlcolor, - match_volatility=match_volatility, - log_scale=log_scale, - resample=None, - title=title, - figsize=figsize, - grayscale=grayscale, - ylabel=ylabel, - subtitle=subtitle, - savefig=savefig, show=show) + returns = returns.resample("A").apply(_df.sum) + returns = returns.resample("A").last() + + fig = _core.plot_returns_bars( + returns, + benchmark, + fontname=fontname, + hline=returns.mean(), + hlw=hlw, + hllabel=hllabel, + hlcolor=hlcolor, + match_volatility=match_volatility, + log_scale=log_scale, + resample=None, + title=title, + figsize=figsize, + grayscale=grayscale, + ylabel=ylabel, + subtitle=subtitle, + savefig=savefig, + show=show, + ) if not show: return fig -def distribution(returns, fontname='Arial', grayscale=False, ylabel=True, - figsize=(10, 6), subtitle=True, compounded=True, - savefig=None, show=True, title=None, - prepare_returns=True): +def distribution( + returns, + fontname="Arial", + grayscale=False, + ylabel=True, + figsize=(10, 6), + subtitle=True, + compounded=True, + savefig=None, + show=True, + title=None, + prepare_returns=True, +): if prepare_returns: returns = _utils._prepare_returns(returns) - fig = _core.plot_distribution(returns, - fontname=fontname, - grayscale=grayscale, - figsize=figsize, - ylabel=ylabel, - subtitle=subtitle, - title=title, - compounded=compounded, - savefig=savefig, show=show) + fig = _core.plot_distribution( + returns, + fontname=fontname, + grayscale=grayscale, + figsize=figsize, + ylabel=ylabel, + subtitle=subtitle, + title=title, + compounded=compounded, + savefig=savefig, + show=show, + ) if not show: return fig -def histogram(returns, benchmark=None, resample='M', fontname='Arial', - grayscale=False, figsize=(10, 5), ylabel=True, - subtitle=True, compounded=True, savefig=None, show=True, - prepare_returns=True): +def histogram( + returns, + benchmark=None, + resample="M", + fontname="Arial", + grayscale=False, + figsize=(10, 5), + ylabel=True, + subtitle=True, + compounded=True, + savefig=None, + show=True, + prepare_returns=True, +): if prepare_returns: returns = _utils._prepare_returns(returns) if benchmark is not None: benchmark = _utils._prepare_returns(benchmark) - if resample == 'W': + if resample == "W": title = "Weekly " - elif resample == 'M': + elif resample == "M": title = "Monthly " - elif resample == 'Q': + elif resample == "Q": title = "Quarterly " - elif resample == 'A': + elif resample == "A": title = "Annual " else: title = "" - return _core.plot_histogram(returns, - benchmark, - resample=resample, - grayscale=grayscale, - fontname=fontname, - title="Distribution of %sReturns" % title, - figsize=figsize, - ylabel=ylabel, - subtitle=subtitle, - compounded=compounded, - savefig=savefig, show=show) - - -def drawdown(returns, grayscale=False, figsize=(10, 5), - fontname='Arial', lw=1, log_scale=False, - match_volatility=False, compound=False, ylabel="Drawdown", - resample=None, subtitle=True, savefig=None, show=True): + return _core.plot_histogram( + returns, + benchmark, + resample=resample, + grayscale=grayscale, + fontname=fontname, + title="Distribution of %sReturns" % title, + figsize=figsize, + ylabel=ylabel, + subtitle=subtitle, + compounded=compounded, + savefig=savefig, + show=show, + ) + + +def drawdown( + returns, + grayscale=False, + figsize=(10, 5), + fontname="Arial", + lw=1, + log_scale=False, + match_volatility=False, + compound=False, + ylabel="Drawdown", + resample=None, + subtitle=True, + savefig=None, + show=True, +): dd = _stats.to_drawdown_series(returns) - fig = _core.plot_timeseries(dd, title='Underwater Plot', - hline=dd.mean(), hlw=2, hllabel="Average", - returns_label="Drawdown", - compound=compound, match_volatility=match_volatility, - log_scale=log_scale, resample=resample, - fill=True, lw=lw, figsize=figsize, - ylabel=ylabel, - fontname=fontname, grayscale=grayscale, - subtitle=subtitle, - savefig=savefig, show=show) + fig = _core.plot_timeseries( + dd, + title="Underwater Plot", + hline=dd.mean(), + hlw=2, + hllabel="Average", + returns_label="Drawdown", + compound=compound, + match_volatility=match_volatility, + log_scale=log_scale, + resample=resample, + fill=True, + lw=lw, + figsize=figsize, + ylabel=ylabel, + fontname=fontname, + grayscale=grayscale, + subtitle=subtitle, + savefig=savefig, + show=show, + ) if not show: return fig -def drawdowns_periods(returns, periods=5, lw=1.5, log_scale=False, - fontname='Arial', grayscale=False, title=None, figsize=(10, 5), - ylabel=True, subtitle=True, compounded=True, - savefig=None, show=True, - prepare_returns=True): +def drawdowns_periods( + returns, + periods=5, + lw=1.5, + log_scale=False, + fontname="Arial", + grayscale=False, + title=None, + figsize=(10, 5), + ylabel=True, + subtitle=True, + compounded=True, + savefig=None, + show=True, + prepare_returns=True, +): if prepare_returns: returns = _utils._prepare_returns(returns) - fig = _core.plot_longest_drawdowns(returns, - periods=periods, - lw=lw, - log_scale=log_scale, - fontname=fontname, - grayscale=grayscale, - title=title, - figsize=figsize, - ylabel=ylabel, - subtitle=subtitle, - compounded=compounded, - savefig=savefig, show=show) + fig = _core.plot_longest_drawdowns( + returns, + periods=periods, + lw=lw, + log_scale=log_scale, + fontname=fontname, + grayscale=grayscale, + title=title, + figsize=figsize, + ylabel=ylabel, + subtitle=subtitle, + compounded=compounded, + savefig=savefig, + show=show, + ) if not show: return fig -def rolling_beta(returns, benchmark, - window1=126, window1_label="6-Months", - window2=252, window2_label="12-Months", - lw=1.5, fontname='Arial', grayscale=False, - figsize=(10, 3), ylabel=True, - subtitle=True, savefig=None, show=True, - prepare_returns=True): +def rolling_beta( + returns, + benchmark, + window1=126, + window1_label="6-Months", + window2=252, + window2_label="12-Months", + lw=1.5, + fontname="Arial", + grayscale=False, + figsize=(10, 3), + ylabel=True, + subtitle=True, + savefig=None, + show=True, + prepare_returns=True, +): if prepare_returns: returns = _utils._prepare_returns(returns) benchmark = _utils._prepare_benchmark(benchmark, returns.index) - fig = _core.plot_rolling_beta(returns, benchmark, - window1=window1, window1_label=window1_label, - window2=window2, window2_label=window2_label, - title="Rolling Beta to Benchmark", - fontname=fontname, - grayscale=grayscale, - lw=lw, - figsize=figsize, - ylabel=ylabel, - subtitle=subtitle, - savefig=savefig, show=show) + fig = _core.plot_rolling_beta( + returns, + benchmark, + window1=window1, + window1_label=window1_label, + window2=window2, + window2_label=window2_label, + title="Rolling Beta to Benchmark", + fontname=fontname, + grayscale=grayscale, + lw=lw, + figsize=figsize, + ylabel=ylabel, + subtitle=subtitle, + savefig=savefig, + show=show, + ) if not show: return fig -def rolling_volatility(returns, benchmark=None, - period=126, period_label="6-Months", - periods_per_year=252, - lw=1.5, fontname='Arial', grayscale=False, - figsize=(10, 3), ylabel="Volatility", - subtitle=True, savefig=None, show=True): +def rolling_volatility( + returns, + benchmark=None, + period=126, + period_label="6-Months", + periods_per_year=252, + lw=1.5, + fontname="Arial", + grayscale=False, + figsize=(10, 3), + ylabel="Volatility", + subtitle=True, + savefig=None, + show=True, +): returns = _stats.rolling_volatility(returns, period, periods_per_year) if benchmark is not None: benchmark = _utils._prepare_benchmark(benchmark, returns.index) benchmark = _stats.rolling_volatility( - benchmark, period, periods_per_year, prepare_returns=False) - - fig = _core.plot_rolling_stats(returns, benchmark, - hline=returns.mean(), - hlw=1.5, - ylabel=ylabel, - title='Rolling Volatility (%s)' % period_label, - fontname=fontname, - grayscale=grayscale, - lw=lw, - figsize=figsize, - subtitle=subtitle, - savefig=savefig, show=show) + benchmark, period, periods_per_year, prepare_returns=False + ) + + fig = _core.plot_rolling_stats( + returns, + benchmark, + hline=returns.mean(), + hlw=1.5, + ylabel=ylabel, + title="Rolling Volatility (%s)" % period_label, + fontname=fontname, + grayscale=grayscale, + lw=lw, + figsize=figsize, + subtitle=subtitle, + savefig=savefig, + show=show, + ) if not show: return fig -def rolling_sharpe(returns, benchmark=None, rf=0., - period=126, period_label="6-Months", - periods_per_year=252, - lw=1.25, fontname='Arial', grayscale=False, - figsize=(10, 3), ylabel="Sharpe", - subtitle=True, savefig=None, show=True): +def rolling_sharpe( + returns, + benchmark=None, + rf=0.0, + period=126, + period_label="6-Months", + periods_per_year=252, + lw=1.25, + fontname="Arial", + grayscale=False, + figsize=(10, 3), + ylabel="Sharpe", + subtitle=True, + savefig=None, + show=True, +): returns = _stats.rolling_sharpe( - returns, rf, period, True, periods_per_year, ) + returns, + rf, + period, + True, + periods_per_year, + ) if benchmark is not None: benchmark = _utils._prepare_benchmark(benchmark, returns.index, rf) benchmark = _stats.rolling_sharpe( - benchmark, rf, period, True, periods_per_year, - prepare_returns=False) - - fig = _core.plot_rolling_stats(returns, benchmark, - hline=returns.mean(), - hlw=1.5, - ylabel=ylabel, - title='Rolling Sharpe (%s)' % period_label, - fontname=fontname, - grayscale=grayscale, - lw=lw, - figsize=figsize, - subtitle=subtitle, - savefig=savefig, show=show) + benchmark, rf, period, True, periods_per_year, prepare_returns=False + ) + + fig = _core.plot_rolling_stats( + returns, + benchmark, + hline=returns.mean(), + hlw=1.5, + ylabel=ylabel, + title="Rolling Sharpe (%s)" % period_label, + fontname=fontname, + grayscale=grayscale, + lw=lw, + figsize=figsize, + subtitle=subtitle, + savefig=savefig, + show=show, + ) if not show: return fig -def rolling_sortino(returns, benchmark=None, rf=0., - period=126, period_label="6-Months", - periods_per_year=252, - lw=1.25, fontname='Arial', grayscale=False, - figsize=(10, 3), ylabel="Sortino", - subtitle=True, savefig=None, show=True): - - returns = _stats.rolling_sortino( - returns, rf, period, True, periods_per_year) +def rolling_sortino( + returns, + benchmark=None, + rf=0.0, + period=126, + period_label="6-Months", + periods_per_year=252, + lw=1.25, + fontname="Arial", + grayscale=False, + figsize=(10, 3), + ylabel="Sortino", + subtitle=True, + savefig=None, + show=True, +): + + returns = _stats.rolling_sortino(returns, rf, period, True, periods_per_year) if benchmark is not None: benchmark = _utils._prepare_benchmark(benchmark, returns.index, rf) benchmark = _stats.rolling_sortino( - benchmark, rf, period, True, periods_per_year, - prepare_returns=False) - - fig = _core.plot_rolling_stats(returns, benchmark, - hline=returns.mean(), - hlw=1.5, - ylabel=ylabel, - title='Rolling Sortino (%s)' % period_label, - fontname=fontname, - grayscale=grayscale, - lw=lw, - figsize=figsize, - subtitle=subtitle, - savefig=savefig, show=show) + benchmark, rf, period, True, periods_per_year, prepare_returns=False + ) + + fig = _core.plot_rolling_stats( + returns, + benchmark, + hline=returns.mean(), + hlw=1.5, + ylabel=ylabel, + title="Rolling Sortino (%s)" % period_label, + fontname=fontname, + grayscale=grayscale, + lw=lw, + figsize=figsize, + subtitle=subtitle, + savefig=savefig, + show=show, + ) if not show: return fig -def monthly_heatmap(returns, benchmark, annot_size=10, figsize=(10, 5), - cbar=True, square=False, returns_label="Strategy", - compounded=True, eoy=False, - grayscale=False, fontname='Arial', - ylabel=True, savefig=None, show=True, active=False): +def monthly_heatmap( + returns, + benchmark, + annot_size=10, + figsize=(10, 5), + cbar=True, + square=False, + returns_label="Strategy", + compounded=True, + eoy=False, + grayscale=False, + fontname="Arial", + ylabel=True, + savefig=None, + show=True, + active=False, +): # colors, ls, alpha = _core._get_colors(grayscale) - cmap = 'gray' if grayscale else 'RdYlGn' + cmap = "gray" if grayscale else "RdYlGn" - returns = _stats.monthly_returns(returns, eoy=eoy, - compounded=compounded) * 100 + returns = _stats.monthly_returns(returns, eoy=eoy, compounded=compounded) * 100 fig_height = len(returns) / 2.5 @@ -696,49 +963,77 @@ def monthly_heatmap(returns, benchmark, annot_size=10, figsize=(10, 5), figsize = (figsize[0], max([fig_height, figsize[1]])) if cbar: - figsize = (figsize[0]*1.051, max([fig_height, figsize[1]])) + figsize = (figsize[0] * 1.051, max([fig_height, figsize[1]])) fig, ax = _plt.subplots(figsize=figsize) - ax.spines['top'].set_visible(False) - ax.spines['right'].set_visible(False) - ax.spines['bottom'].set_visible(False) - ax.spines['left'].set_visible(False) + ax.spines["top"].set_visible(False) + ax.spines["right"].set_visible(False) + ax.spines["bottom"].set_visible(False) + ax.spines["left"].set_visible(False) - fig.set_facecolor('white') - ax.set_facecolor('white') + fig.set_facecolor("white") + ax.set_facecolor("white") # _sns.set(font_scale=.9) if active and benchmark is not None: - ax.set_title(f'{returns_label} - Monthly Active Returns (%)\n', fontsize=14, y=.995, - fontname=fontname, fontweight='bold', color='black') - benchmark = _stats.monthly_returns(benchmark, eoy=eoy, - compounded=compounded) * 100 + ax.set_title( + f"{returns_label} - Monthly Active Returns (%)\n", + fontsize=14, + y=0.995, + fontname=fontname, + fontweight="bold", + color="black", + ) + benchmark = ( + _stats.monthly_returns(benchmark, eoy=eoy, compounded=compounded) * 100 + ) active_returns = returns - benchmark - ax = _sns.heatmap(active_returns, ax=ax, annot=True, center=0, - annot_kws={"size": annot_size}, - fmt="0.2f", linewidths=0.5, - square=square, cbar=cbar, cmap=cmap, - cbar_kws={'format': '%.0f%%'}) + ax = _sns.heatmap( + active_returns, + ax=ax, + annot=True, + center=0, + annot_kws={"size": annot_size}, + fmt="0.2f", + linewidths=0.5, + square=square, + cbar=cbar, + cmap=cmap, + cbar_kws={"format": "%.0f%%"}, + ) else: - ax.set_title(f'{returns_label} - Monthly Returns (%)\n', fontsize=14, y=.995, - fontname=fontname, fontweight='bold', color='black') - ax = _sns.heatmap(returns, ax=ax, annot=True, center=0, - annot_kws={"size": annot_size}, - fmt="0.2f", linewidths=0.5, - square=square, cbar=cbar, cmap=cmap, - cbar_kws={'format': '%.0f%%'}) + ax.set_title( + f"{returns_label} - Monthly Returns (%)\n", + fontsize=14, + y=0.995, + fontname=fontname, + fontweight="bold", + color="black", + ) + ax = _sns.heatmap( + returns, + ax=ax, + annot=True, + center=0, + annot_kws={"size": annot_size}, + fmt="0.2f", + linewidths=0.5, + square=square, + cbar=cbar, + cmap=cmap, + cbar_kws={"format": "%.0f%%"}, + ) # _sns.set(font_scale=1) # align plot to match other if ylabel: - ax.set_ylabel('Years', fontname=fontname, - fontweight='bold', fontsize=12) - ax.yaxis.set_label_coords(-.1, .5) + ax.set_ylabel("Years", fontname=fontname, fontweight="bold", fontsize=12) + ax.yaxis.set_label_coords(-0.1, 0.5) ax.tick_params(colors="#808080") - _plt.xticks(rotation=0, fontsize=annot_size*1.2) - _plt.yticks(rotation=0, fontsize=annot_size*1.2) + _plt.xticks(rotation=0, fontsize=annot_size * 1.2) + _plt.yticks(rotation=0, fontsize=annot_size * 1.2) try: _plt.subplots_adjust(hspace=0, bottom=0, top=1) @@ -766,13 +1061,31 @@ def monthly_heatmap(returns, benchmark, annot_size=10, figsize=(10, 5), return None -def monthly_returns(returns, annot_size=10, figsize=(10, 5), - cbar=True, square=False, - compounded=True, eoy=False, - grayscale=False, fontname='Arial', - ylabel=True, savefig=None, show=True): - return monthly_heatmap(returns, annot_size, figsize, - cbar, square, - compounded, eoy, - grayscale, fontname, - ylabel, savefig, show) +def monthly_returns( + returns, + annot_size=10, + figsize=(10, 5), + cbar=True, + square=False, + compounded=True, + eoy=False, + grayscale=False, + fontname="Arial", + ylabel=True, + savefig=None, + show=True, +): + return monthly_heatmap( + returns, + annot_size, + figsize, + cbar, + square, + compounded, + eoy, + grayscale, + fontname, + ylabel, + savefig, + show, + ) diff --git a/quantstats/plots.py b/quantstats/plots.py index d75ed5df..1ebe38ea 100644 --- a/quantstats/plots.py +++ b/quantstats/plots.py @@ -20,6 +20,7 @@ try: from pandas.plotting import register_matplotlib_converters as _rmc + _rmc() except ImportError: pass diff --git a/quantstats/reports.py b/quantstats/reports.py index 8d529f05..5b983db3 100644 --- a/quantstats/reports.py +++ b/quantstats/reports.py @@ -21,30 +21,22 @@ import pandas as _pd import numpy as _np from math import sqrt as _sqrt, ceil as _ceil -from datetime import ( - datetime as _dt, timedelta as _td -) +from datetime import datetime as _dt from base64 import b64encode as _b64encode import re as _regex from tabulate import tabulate as _tabulate -from . import ( - __version__, stats as _stats, - utils as _utils, plots as _plots -) +from . import __version__, stats as _stats, utils as _utils, plots as _plots from dateutil.relativedelta import relativedelta from io import StringIO + try: - from IPython.display import ( - display as iDisplay, HTML as iHTML - ) + from IPython.display import display as iDisplay, HTML as iHTML except ImportError: - from IPython.core.display import ( - display as iDisplay, HTML as iHTML - ) + from IPython.core.display import display as iDisplay, HTML as iHTML def _get_trading_periods(periods_per_year=252): - half_year = _ceil(periods_per_year/2) + half_year = _ceil(periods_per_year / 2) return periods_per_year, half_year @@ -59,10 +51,21 @@ def _match_dates(returns, benchmark): return returns, benchmark -def html(returns, benchmark=None, rf=0., grayscale=False, - title='Strategy Tearsheet', output=None, compounded=True, - periods_per_year=252, download_filename='quantstats-tearsheet.html', - figfmt='svg', template_path=None, match_dates=True, **kwargs): +def html( + returns, + benchmark=None, + rf=0.0, + grayscale=False, + title="Strategy Tearsheet", + output=None, + compounded=True, + periods_per_year=252, + download_filename="quantstats-tearsheet.html", + figfmt="svg", + template_path=None, + match_dates=True, + **kwargs, +): if output is None and not _utils._in_notebook(): raise ValueError("`output` must be specified") @@ -73,7 +76,7 @@ def html(returns, benchmark=None, rf=0., grayscale=False, win_year, win_half_year = _get_trading_periods(periods_per_year) tpl = "" - with open(template_path or __file__[:-4] + '.html') as f: + with open(template_path or __file__[:-4] + ".html") as f: tpl = f.read() f.close() @@ -82,14 +85,14 @@ def html(returns, benchmark=None, rf=0., grayscale=False, returns = returns.dropna() returns = _utils._prepare_returns(returns) - strategy_title = kwargs.get('strategy_title', 'Strategy') + 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 benchmark is not None: - benchmark_title = kwargs.get('benchmark_title', 'Benchmark') - if kwargs.get('benchmark_title') is None: + benchmark_title = kwargs.get("benchmark_title", "Benchmark") + if kwargs.get("benchmark_title") is None: if isinstance(benchmark, str): benchmark_title = benchmark elif isinstance(benchmark, _pd.Series): @@ -97,17 +100,19 @@ def html(returns, benchmark=None, rf=0., grayscale=False, elif isinstance(benchmark, _pd.DataFrame): benchmark_title = benchmark[benchmark.columns[0]].name - tpl = tpl.replace('{{benchmark_title}}', f"Benchmark is {benchmark_title.upper()} | ") + tpl = tpl.replace( + "{{benchmark_title}}", f"Benchmark is {benchmark_title.upper()} | " + ) benchmark = _utils._prepare_benchmark(benchmark, returns.index, rf) if match_dates is True: returns, benchmark = _match_dates(returns, benchmark) else: benchmark_title = None - date_range = returns.index.strftime('%e %b, %Y') - tpl = tpl.replace('{{date_range}}', date_range[0] + ' - ' + date_range[-1]) - tpl = tpl.replace('{{title}}', title) - tpl = tpl.replace('{{v}}', __version__) + date_range = returns.index.strftime("%e %b, %Y") + tpl = tpl.replace("{{date_range}}", date_range[0] + " - " + date_range[-1]) + tpl = tpl.replace("{{title}}", title) + tpl = tpl.replace("{{v}}", __version__) if benchmark is not None: benchmark.name = benchmark_title @@ -116,245 +121,386 @@ def html(returns, benchmark=None, rf=0., grayscale=False, elif isinstance(returns, _pd.DataFrame): returns.columns = strategy_title - mtrx = metrics(returns=returns, benchmark=benchmark, - rf=rf, display=False, mode='full', - sep=True, internal="True", - compounded=compounded, - periods_per_year=periods_per_year, - prepare_returns=False, - benchmark_title=benchmark_title, - strategy_title=strategy_title)[2:] - - mtrx.index.name = 'Metric' - tpl = tpl.replace('{{metrics}}', _html_table(mtrx)) + mtrx = metrics( + returns=returns, + benchmark=benchmark, + rf=rf, + display=False, + mode="full", + sep=True, + internal="True", + compounded=compounded, + periods_per_year=periods_per_year, + prepare_returns=False, + benchmark_title=benchmark_title, + strategy_title=strategy_title, + )[2:] + + mtrx.index.name = "Metric" + tpl = tpl.replace("{{metrics}}", _html_table(mtrx)) if isinstance(returns, _pd.DataFrame): num_cols = len(returns.columns) - for i in reversed(range(num_cols+1, num_cols + 3)): + for i in reversed(range(num_cols + 1, num_cols + 3)): str_td = "" * i - tpl = tpl.replace(f'{str_td}', - '
'.format(i)) + tpl = tpl.replace( + f"{str_td}", '
'.format(i) + ) - tpl = tpl.replace('', - '
') - tpl = tpl.replace('', - '
') + tpl = tpl.replace( + "", '
' + ) + tpl = tpl.replace( + "", '
' + ) if benchmark is not None: yoy = _stats.compare( - returns, benchmark, "A", compounded=compounded, - prepare_returns=False) + returns, benchmark, "A", compounded=compounded, prepare_returns=False + ) if isinstance(returns, _pd.Series): - yoy.columns = [benchmark_title, strategy_title, 'Multiplier', 'Won'] + yoy.columns = [benchmark_title, strategy_title, "Multiplier", "Won"] elif isinstance(returns, _pd.DataFrame): - yoy.columns = list(_pd.core.common.flatten([benchmark_title, strategy_title])) - yoy.index.name = 'Year' - tpl = tpl.replace('{{eoy_title}}', '

EOY Returns vs Benchmark

') - tpl = tpl.replace('{{eoy_table}}', _html_table(yoy)) + yoy.columns = list( + _pd.core.common.flatten([benchmark_title, strategy_title]) + ) + yoy.index.name = "Year" + tpl = tpl.replace("{{eoy_title}}", "

EOY Returns vs Benchmark

") + tpl = tpl.replace("{{eoy_table}}", _html_table(yoy)) else: # pct multiplier - yoy = _pd.DataFrame( - _utils.group_returns(returns, returns.index.year) * 100) + yoy = _pd.DataFrame(_utils.group_returns(returns, returns.index.year) * 100) if isinstance(returns, _pd.Series): - yoy.columns = ['Return'] - yoy['Cumulative'] = _utils.group_returns( - returns, returns.index.year, True) - yoy['Return'] = yoy['Return'].round(2).astype(str) + '%' - yoy['Cumulative'] = (yoy['Cumulative'] * - 100).round(2).astype(str) + '%' + yoy.columns = ["Return"] + yoy["Cumulative"] = _utils.group_returns(returns, returns.index.year, True) + yoy["Return"] = yoy["Return"].round(2).astype(str) + "%" + yoy["Cumulative"] = (yoy["Cumulative"] * 100).round(2).astype(str) + "%" elif isinstance(returns, _pd.DataFrame): # Don't show cumulative for multiple strategy portfolios # just show compounded like when we have a benchmark yoy.columns = list(_pd.core.common.flatten(strategy_title)) - yoy.index.name = 'Year' - tpl = tpl.replace('{{eoy_title}}', '

EOY Returns

') - tpl = tpl.replace('{{eoy_table}}', _html_table(yoy)) + yoy.index.name = "Year" + tpl = tpl.replace("{{eoy_title}}", "

EOY Returns

") + tpl = tpl.replace("{{eoy_table}}", _html_table(yoy)) if isinstance(returns, _pd.Series): dd = _stats.to_drawdown_series(returns) dd_info = _stats.drawdown_details(dd).sort_values( - by='max drawdown', ascending=True)[:10] - dd_info = dd_info[['start', 'end', 'max drawdown', 'days']] - dd_info.columns = ['Started', 'Recovered', 'Drawdown', 'Days'] - tpl = tpl.replace('{{dd_info}}', _html_table(dd_info, False)) + by="max drawdown", ascending=True + )[:10] + dd_info = dd_info[["start", "end", "max drawdown", "days"]] + dd_info.columns = ["Started", "Recovered", "Drawdown", "Days"] + tpl = tpl.replace("{{dd_info}}", _html_table(dd_info, False)) elif isinstance(returns, _pd.DataFrame): dd_info_list = [] for col in returns.columns: dd = _stats.to_drawdown_series(returns[col]) dd_info = _stats.drawdown_details(dd).sort_values( - by='max drawdown', ascending=True)[:10] - dd_info = dd_info[['start', 'end', 'max drawdown', 'days']] - dd_info.columns = ['Started', 'Recovered', 'Drawdown', 'Days'] + by="max drawdown", ascending=True + )[:10] + dd_info = dd_info[["start", "end", "max drawdown", "days"]] + dd_info.columns = ["Started", "Recovered", "Drawdown", "Days"] dd_info_list.append(_html_table(dd_info, False)) dd_html_table = "" for html_str, col in zip(dd_info_list, returns.columns): - dd_html_table = dd_html_table + f"

{col}


" + StringIO(html_str).read() - tpl = tpl.replace('{{dd_info}}', dd_html_table) + dd_html_table = ( + dd_html_table + f"

{col}


" + StringIO(html_str).read() + ) + tpl = tpl.replace("{{dd_info}}", dd_html_table) - active = kwargs.get('active_returns', 'False') + active = kwargs.get("active_returns", "False") # plots figfile = _utils._file_stream() - _plots.returns(returns, benchmark, grayscale=grayscale, - figsize=(8, 5), subtitle=False, - savefig={'fname': figfile, 'format': figfmt}, - show=False, ylabel=False, cumulative=compounded, - prepare_returns=False) - tpl = tpl.replace('{{returns}}', _embed_figure(figfile, figfmt)) + _plots.returns( + returns, + benchmark, + grayscale=grayscale, + figsize=(8, 5), + subtitle=False, + savefig={"fname": figfile, "format": figfmt}, + show=False, + ylabel=False, + cumulative=compounded, + prepare_returns=False, + ) + tpl = tpl.replace("{{returns}}", _embed_figure(figfile, figfmt)) figfile = _utils._file_stream() - _plots.log_returns(returns, benchmark, grayscale=grayscale, - figsize=(8, 4), subtitle=False, - savefig={'fname': figfile, 'format': figfmt}, - show=False, ylabel=False, cumulative=compounded, - prepare_returns=False) - tpl = tpl.replace('{{log_returns}}', _embed_figure(figfile, figfmt)) + _plots.log_returns( + returns, + benchmark, + grayscale=grayscale, + figsize=(8, 4), + subtitle=False, + savefig={"fname": figfile, "format": figfmt}, + show=False, + ylabel=False, + cumulative=compounded, + prepare_returns=False, + ) + tpl = tpl.replace("{{log_returns}}", _embed_figure(figfile, figfmt)) if benchmark is not None: figfile = _utils._file_stream() - _plots.returns(returns, benchmark, match_volatility=True, - grayscale=grayscale, figsize=(8, 4), subtitle=False, - savefig={'fname': figfile, 'format': figfmt}, - show=False, ylabel=False, cumulative=compounded, - prepare_returns=False) - tpl = tpl.replace('{{vol_returns}}', _embed_figure(figfile, figfmt)) + _plots.returns( + returns, + benchmark, + match_volatility=True, + grayscale=grayscale, + figsize=(8, 4), + subtitle=False, + savefig={"fname": figfile, "format": figfmt}, + show=False, + ylabel=False, + cumulative=compounded, + prepare_returns=False, + ) + tpl = tpl.replace("{{vol_returns}}", _embed_figure(figfile, figfmt)) figfile = _utils._file_stream() - _plots.yearly_returns(returns, benchmark, grayscale=grayscale, - figsize=(8, 4), subtitle=False, - savefig={'fname': figfile, 'format': figfmt}, - show=False, ylabel=False, compounded=compounded, - prepare_returns=False) - tpl = tpl.replace('{{eoy_returns}}', _embed_figure(figfile, figfmt)) + _plots.yearly_returns( + returns, + benchmark, + grayscale=grayscale, + figsize=(8, 4), + subtitle=False, + savefig={"fname": figfile, "format": figfmt}, + show=False, + ylabel=False, + compounded=compounded, + prepare_returns=False, + ) + tpl = tpl.replace("{{eoy_returns}}", _embed_figure(figfile, figfmt)) figfile = _utils._file_stream() - _plots.histogram(returns, benchmark, grayscale=grayscale, - figsize=(7, 4), subtitle=False, - savefig={'fname': figfile, 'format': figfmt}, - show=False, ylabel=False, compounded=compounded, - prepare_returns=False) - tpl = tpl.replace('{{monthly_dist}}', _embed_figure(figfile, figfmt)) + _plots.histogram( + returns, + benchmark, + grayscale=grayscale, + figsize=(7, 4), + subtitle=False, + savefig={"fname": figfile, "format": figfmt}, + show=False, + ylabel=False, + compounded=compounded, + prepare_returns=False, + ) + tpl = tpl.replace("{{monthly_dist}}", _embed_figure(figfile, figfmt)) figfile = _utils._file_stream() - _plots.daily_returns(returns, benchmark, grayscale=grayscale, - figsize=(8, 3), subtitle=False, - savefig={'fname': figfile, 'format': figfmt}, - show=False, ylabel=False, - prepare_returns=False, active=active) - tpl = tpl.replace('{{daily_returns}}', _embed_figure(figfile, figfmt)) + _plots.daily_returns( + returns, + benchmark, + grayscale=grayscale, + figsize=(8, 3), + subtitle=False, + savefig={"fname": figfile, "format": figfmt}, + show=False, + ylabel=False, + prepare_returns=False, + active=active, + ) + tpl = tpl.replace("{{daily_returns}}", _embed_figure(figfile, figfmt)) if benchmark is not None: figfile = _utils._file_stream() - _plots.rolling_beta(returns, benchmark, grayscale=grayscale, - figsize=(8, 3), subtitle=False, - window1=win_half_year, window2=win_year, - savefig={'fname': figfile, 'format': figfmt}, - show=False, ylabel=False, - prepare_returns=False) - tpl = tpl.replace('{{rolling_beta}}', _embed_figure(figfile, figfmt)) + _plots.rolling_beta( + returns, + benchmark, + grayscale=grayscale, + figsize=(8, 3), + subtitle=False, + window1=win_half_year, + window2=win_year, + savefig={"fname": figfile, "format": figfmt}, + show=False, + ylabel=False, + prepare_returns=False, + ) + tpl = tpl.replace("{{rolling_beta}}", _embed_figure(figfile, figfmt)) figfile = _utils._file_stream() - _plots.rolling_volatility(returns, benchmark, grayscale=grayscale, - figsize=(8, 3), subtitle=False, - savefig={'fname': figfile, 'format': figfmt}, - show=False, ylabel=False, period=win_half_year, - periods_per_year=win_year) - tpl = tpl.replace('{{rolling_vol}}', _embed_figure(figfile, figfmt)) + _plots.rolling_volatility( + returns, + benchmark, + grayscale=grayscale, + figsize=(8, 3), + subtitle=False, + savefig={"fname": figfile, "format": figfmt}, + show=False, + ylabel=False, + period=win_half_year, + periods_per_year=win_year, + ) + tpl = tpl.replace("{{rolling_vol}}", _embed_figure(figfile, figfmt)) figfile = _utils._file_stream() - _plots.rolling_sharpe(returns, grayscale=grayscale, - figsize=(8, 3), subtitle=False, - savefig={'fname': figfile, 'format': figfmt}, - show=False, ylabel=False, period=win_half_year, - periods_per_year=win_year) - tpl = tpl.replace('{{rolling_sharpe}}', _embed_figure(figfile, figfmt)) + _plots.rolling_sharpe( + returns, + grayscale=grayscale, + figsize=(8, 3), + subtitle=False, + savefig={"fname": figfile, "format": figfmt}, + show=False, + ylabel=False, + period=win_half_year, + periods_per_year=win_year, + ) + tpl = tpl.replace("{{rolling_sharpe}}", _embed_figure(figfile, figfmt)) figfile = _utils._file_stream() - _plots.rolling_sortino(returns, grayscale=grayscale, - figsize=(8, 3), subtitle=False, - savefig={'fname': figfile, 'format': figfmt}, - show=False, ylabel=False, period=win_half_year, - periods_per_year=win_year) - tpl = tpl.replace('{{rolling_sortino}}', _embed_figure(figfile, figfmt)) + _plots.rolling_sortino( + returns, + grayscale=grayscale, + figsize=(8, 3), + subtitle=False, + savefig={"fname": figfile, "format": figfmt}, + show=False, + ylabel=False, + period=win_half_year, + periods_per_year=win_year, + ) + tpl = tpl.replace("{{rolling_sortino}}", _embed_figure(figfile, figfmt)) figfile = _utils._file_stream() if isinstance(returns, _pd.Series): - _plots.drawdowns_periods(returns, grayscale=grayscale, - figsize=(8, 4), subtitle=False, title=returns.name, - savefig={'fname': figfile, 'format': figfmt}, - show=False, ylabel=False, compounded=compounded, - prepare_returns=False) - tpl = tpl.replace('{{dd_periods}}', _embed_figure(figfile, figfmt)) + _plots.drawdowns_periods( + returns, + grayscale=grayscale, + figsize=(8, 4), + subtitle=False, + title=returns.name, + savefig={"fname": figfile, "format": figfmt}, + show=False, + ylabel=False, + compounded=compounded, + prepare_returns=False, + ) + tpl = tpl.replace("{{dd_periods}}", _embed_figure(figfile, figfmt)) elif isinstance(returns, _pd.DataFrame): embed = [] for col in returns.columns: - _plots.drawdowns_periods(returns[col], grayscale=grayscale, - figsize=(8, 4), subtitle=False, title=col, - savefig={'fname': figfile, 'format': figfmt}, - show=False, ylabel=False, compounded=compounded, - prepare_returns=False) + _plots.drawdowns_periods( + returns[col], + grayscale=grayscale, + figsize=(8, 4), + subtitle=False, + title=col, + savefig={"fname": figfile, "format": figfmt}, + show=False, + ylabel=False, + compounded=compounded, + prepare_returns=False, + ) embed.append(figfile) - tpl = tpl.replace('{{dd_periods}}', _embed_figure(embed, figfmt)) + tpl = tpl.replace("{{dd_periods}}", _embed_figure(embed, figfmt)) figfile = _utils._file_stream() - _plots.drawdown(returns, grayscale=grayscale, - figsize=(8, 3), subtitle=False, - savefig={'fname': figfile, 'format': figfmt}, - show=False, ylabel=False) - tpl = tpl.replace('{{dd_plot}}', _embed_figure(figfile, figfmt)) + _plots.drawdown( + returns, + grayscale=grayscale, + figsize=(8, 3), + subtitle=False, + savefig={"fname": figfile, "format": figfmt}, + show=False, + ylabel=False, + ) + tpl = tpl.replace("{{dd_plot}}", _embed_figure(figfile, figfmt)) figfile = _utils._file_stream() if isinstance(returns, _pd.Series): - _plots.monthly_heatmap(returns, benchmark, grayscale=grayscale, - figsize=(8, 4), cbar=False, returns_label=returns.name, - savefig={'fname': figfile, 'format': figfmt}, - show=False, ylabel=False, compounded=compounded, active=active) - tpl = tpl.replace('{{monthly_heatmap}}', _embed_figure(figfile, figfmt)) + _plots.monthly_heatmap( + returns, + benchmark, + grayscale=grayscale, + figsize=(8, 4), + cbar=False, + returns_label=returns.name, + savefig={"fname": figfile, "format": figfmt}, + show=False, + ylabel=False, + compounded=compounded, + active=active, + ) + tpl = tpl.replace("{{monthly_heatmap}}", _embed_figure(figfile, figfmt)) elif isinstance(returns, _pd.DataFrame): embed = [] for col in returns.columns: - _plots.monthly_heatmap(returns[col], benchmark, grayscale=grayscale, - figsize=(8, 4), cbar=False, returns_label=col, - savefig={'fname': figfile, 'format': figfmt}, - show=False, ylabel=False, compounded=compounded, active=active) + _plots.monthly_heatmap( + returns[col], + benchmark, + grayscale=grayscale, + figsize=(8, 4), + cbar=False, + returns_label=col, + savefig={"fname": figfile, "format": figfmt}, + show=False, + ylabel=False, + compounded=compounded, + active=active, + ) embed.append(figfile) - tpl = tpl.replace('{{monthly_heatmap}}', _embed_figure(embed, figfmt)) + tpl = tpl.replace("{{monthly_heatmap}}", _embed_figure(embed, figfmt)) figfile = _utils._file_stream() if isinstance(returns, _pd.Series): - _plots.distribution(returns, grayscale=grayscale, - figsize=(8, 4), subtitle=False, title=returns.name, - savefig={'fname': figfile, 'format': figfmt}, - show=False, ylabel=False, compounded=compounded, - prepare_returns=False) - tpl = tpl.replace('{{returns_dist}}', _embed_figure(figfile, figfmt)) + _plots.distribution( + returns, + grayscale=grayscale, + figsize=(8, 4), + subtitle=False, + title=returns.name, + savefig={"fname": figfile, "format": figfmt}, + show=False, + ylabel=False, + compounded=compounded, + prepare_returns=False, + ) + tpl = tpl.replace("{{returns_dist}}", _embed_figure(figfile, figfmt)) elif isinstance(returns, _pd.DataFrame): embed = [] for col in returns.columns: - _plots.distribution(returns[col], grayscale=grayscale, - figsize=(8, 4), subtitle=False, title=col, - savefig={'fname': figfile, 'format': figfmt}, - show=False, ylabel=False, compounded=compounded, - prepare_returns=False) + _plots.distribution( + returns[col], + grayscale=grayscale, + figsize=(8, 4), + subtitle=False, + title=col, + savefig={"fname": figfile, "format": figfmt}, + show=False, + ylabel=False, + compounded=compounded, + prepare_returns=False, + ) embed.append(figfile) - tpl = tpl.replace('{{returns_dist}}', _embed_figure(embed, figfmt)) + tpl = tpl.replace("{{returns_dist}}", _embed_figure(embed, figfmt)) - tpl = _regex.sub(r'\{\{(.*?)\}\}', '', tpl) - tpl = tpl.replace('white-space:pre;', '') + tpl = _regex.sub(r"\{\{(.*?)\}\}", "", tpl) + tpl = tpl.replace("white-space:pre;", "") if output is None: # _open_html(tpl) _download_html(tpl, download_filename) return - with open(output, 'w', encoding='utf-8') as f: + with open(output, "w", encoding="utf-8") as f: f.write(tpl) -def full(returns, benchmark=None, rf=0., grayscale=False, - figsize=(8, 5), display=True, compounded=True, - periods_per_year=252, match_dates=True, **kwargs): +def full( + returns, + benchmark=None, + rf=0.0, + grayscale=False, + figsize=(8, 5), + display=True, + compounded=True, + periods_per_year=252, + match_dates=True, + **kwargs, +): # prepare timeseries if match_dates: @@ -367,9 +513,9 @@ def full(returns, benchmark=None, rf=0., grayscale=False, benchmark_title = None if benchmark is not None: - benchmark_title = kwargs.get('benchmark_title', 'Benchmark') - strategy_title = kwargs.get('strategy_title', 'Strategy') - active = kwargs.get('active_returns', 'False') + benchmark_title = kwargs.get("benchmark_title", "Benchmark") + 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): @@ -386,8 +532,7 @@ def full(returns, benchmark=None, rf=0., grayscale=False, if isinstance(dd, _pd.Series): col = _stats.drawdown_details(dd).columns[4] - dd_info = _stats.drawdown_details(dd).sort_values(by=col, - ascending=True)[:5] + dd_info = _stats.drawdown_details(dd).sort_values(by=col, ascending=True)[:5] if not dd_info.empty: dd_info.index = range(1, min(6, len(dd_info) + 1)) dd_info.columns = map(lambda x: str(x).title(), dd_info.columns) @@ -395,22 +540,30 @@ def full(returns, benchmark=None, rf=0., grayscale=False, col = _stats.drawdown_details(dd).columns.get_level_values(1)[4] dd_info_dict = {} for ptf in dd.columns: - dd_info = _stats.drawdown_details(dd[ptf]).sort_values(by=col, - ascending=True)[:5] + dd_info = _stats.drawdown_details(dd[ptf]).sort_values( + by=col, ascending=True + )[:5] if not dd_info.empty: dd_info.index = range(1, min(6, len(dd_info) + 1)) dd_info.columns = map(lambda x: str(x).title(), dd_info.columns) dd_info_dict[ptf] = dd_info if _utils._in_notebook(): - iDisplay(iHTML('

Performance Metrics

')) - iDisplay(metrics(returns=returns, benchmark=benchmark, - rf=rf, display=display, mode='full', - compounded=compounded, - periods_per_year=periods_per_year, - prepare_returns=False, - benchmark_title=benchmark_title, - strategy_title=strategy_title)) + iDisplay(iHTML("

Performance Metrics

")) + iDisplay( + metrics( + returns=returns, + benchmark=benchmark, + rf=rf, + display=display, + mode="full", + compounded=compounded, + periods_per_year=periods_per_year, + prepare_returns=False, + benchmark_title=benchmark_title, + strategy_title=strategy_title, + ) + ) if isinstance(dd, _pd.Series): iDisplay(iHTML('

Worst 5 Drawdowns

')) @@ -420,51 +573,84 @@ def full(returns, benchmark=None, rf=0., grayscale=False, iDisplay(dd_info) elif isinstance(dd, _pd.DataFrame): for ptf, dd_info in dd_info_dict.items(): - iDisplay(iHTML('

%s - Worst 5 Drawdowns

' % ptf)) + iDisplay( + iHTML( + '

%s - Worst 5 Drawdowns

' + % ptf + ) + ) if dd_info.empty: iDisplay(iHTML("

(no drawdowns)

")) else: iDisplay(dd_info) - iDisplay(iHTML('

Strategy Visualization

')) + iDisplay(iHTML("

Strategy Visualization

")) else: - print('[Performance Metrics]\n') - metrics(returns=returns, benchmark=benchmark, - rf=rf, display=display, mode='full', - compounded=compounded, - periods_per_year=periods_per_year, - prepare_returns=False, - benchmark_title=benchmark_title, - strategy_title=strategy_title) - print('\n\n') - print('[Worst 5 Drawdowns]\n') + print("[Performance Metrics]\n") + metrics( + returns=returns, + benchmark=benchmark, + rf=rf, + display=display, + mode="full", + compounded=compounded, + periods_per_year=periods_per_year, + prepare_returns=False, + benchmark_title=benchmark_title, + strategy_title=strategy_title, + ) + print("\n\n") + print("[Worst 5 Drawdowns]\n") if isinstance(dd, _pd.Series): if dd_info.empty: print("(no drawdowns)") else: - print(_tabulate(dd_info, headers="keys", - tablefmt='simple', floatfmt=".2f")) + print( + _tabulate( + dd_info, headers="keys", tablefmt="simple", floatfmt=".2f" + ) + ) elif isinstance(dd, _pd.DataFrame): for ptf, dd_info in dd_info_dict.items(): if dd_info.empty: print("(no drawdowns)") else: print(f"{ptf}\n") - print(_tabulate(dd_info, headers="keys", - tablefmt='simple', floatfmt=".2f")) - - print('\n\n') - print('[Strategy Visualization]\nvia Matplotlib') - - plots(returns=returns, benchmark=benchmark, - grayscale=grayscale, figsize=figsize, mode='full', - periods_per_year=periods_per_year, prepare_returns=False, - benchmark_title=benchmark_title, strategy_title=strategy_title, active=active) + print( + _tabulate( + dd_info, headers="keys", tablefmt="simple", floatfmt=".2f" + ) + ) + + print("\n\n") + print("[Strategy Visualization]\nvia Matplotlib") + + plots( + returns=returns, + benchmark=benchmark, + grayscale=grayscale, + figsize=figsize, + mode="full", + periods_per_year=periods_per_year, + prepare_returns=False, + benchmark_title=benchmark_title, + strategy_title=strategy_title, + active=active, + ) -def basic(returns, benchmark=None, rf=0., grayscale=False, - figsize=(8, 5), display=True, compounded=True, - periods_per_year=252, match_dates=True, **kwargs): +def basic( + returns, + benchmark=None, + rf=0.0, + grayscale=False, + figsize=(8, 5), + display=True, + compounded=True, + periods_per_year=252, + match_dates=True, + **kwargs, +): # prepare timeseries if match_dates: @@ -477,46 +663,74 @@ def basic(returns, benchmark=None, rf=0., grayscale=False, benchmark_title = None if benchmark is not None: - benchmark_title = kwargs.get('benchmark_title', 'Benchmark') - strategy_title = kwargs.get('strategy_title', 'Strategy') - active = kwargs.get('active_returns', 'False') + benchmark_title = kwargs.get("benchmark_title", "Benchmark") + 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 _utils._in_notebook(): - iDisplay(iHTML('

Performance Metrics

')) - metrics(returns=returns, benchmark=benchmark, - rf=rf, display=display, mode='basic', - compounded=compounded, - periods_per_year=periods_per_year, - prepare_returns=False, - benchmark_title=benchmark_title, strategy_title=strategy_title) - iDisplay(iHTML('

Strategy Visualization

')) + iDisplay(iHTML("

Performance Metrics

")) + metrics( + returns=returns, + benchmark=benchmark, + rf=rf, + display=display, + mode="basic", + compounded=compounded, + periods_per_year=periods_per_year, + prepare_returns=False, + benchmark_title=benchmark_title, + strategy_title=strategy_title, + ) + iDisplay(iHTML("

Strategy Visualization

")) else: - print('[Performance Metrics]\n') - metrics(returns=returns, benchmark=benchmark, - rf=rf, display=display, mode='basic', - compounded=compounded, - periods_per_year=periods_per_year, - prepare_returns=False, - benchmark_title=benchmark_title, strategy_title=strategy_title) - - print('\n\n') - print('[Strategy Visualization]\nvia Matplotlib') - - plots(returns=returns, benchmark=benchmark, - grayscale=grayscale, figsize=figsize, mode='basic', - periods_per_year=periods_per_year, - prepare_returns=False, benchmark_title=benchmark_title, - strategy_title=strategy_title, active=active) + print("[Performance Metrics]\n") + metrics( + returns=returns, + benchmark=benchmark, + rf=rf, + display=display, + mode="basic", + compounded=compounded, + periods_per_year=periods_per_year, + prepare_returns=False, + benchmark_title=benchmark_title, + strategy_title=strategy_title, + ) + + print("\n\n") + print("[Strategy Visualization]\nvia Matplotlib") + + plots( + returns=returns, + benchmark=benchmark, + grayscale=grayscale, + figsize=figsize, + mode="basic", + periods_per_year=periods_per_year, + prepare_returns=False, + benchmark_title=benchmark_title, + strategy_title=strategy_title, + active=active, + ) -def metrics(returns, benchmark=None, rf=0., display=True, - mode='basic', sep=False, compounded=True, - periods_per_year=252, prepare_returns=True, - match_dates=True, **kwargs): +def metrics( + returns, + benchmark=None, + rf=0.0, + display=True, + mode="basic", + sep=False, + compounded=True, + periods_per_year=252, + prepare_returns=True, + match_dates=True, + **kwargs, +): if match_dates: returns = returns.dropna() @@ -528,18 +742,20 @@ def metrics(returns, benchmark=None, rf=0., display=True, if benchmark is not None: if isinstance(benchmark, str): - benchmark_colname = f'Benchmark ({benchmark.upper()})' + benchmark_colname = f"Benchmark ({benchmark.upper()})" elif isinstance(benchmark, _pd.DataFrame) and len(benchmark.columns) > 1: - raise ValueError("`benchmark` must be a pandas Series, " - "but a multi-column DataFrame was passed") + raise ValueError( + "`benchmark` must be a pandas Series, " + "but a multi-column DataFrame was passed" + ) if isinstance(returns, _pd.DataFrame): if len(returns.columns) > 1: - blank = [''] * len(returns.columns) + blank = [""] * len(returns.columns) if isinstance(strategy_colname, str): strategy_colname = list(returns.columns) else: - blank = [''] + blank = [""] # if isinstance(returns, _pd.DataFrame): # if len(returns.columns) > 1: @@ -552,8 +768,12 @@ def metrics(returns, benchmark=None, rf=0., display=True, if isinstance(returns, _pd.Series): df = _pd.DataFrame({"returns": returns}) elif isinstance(returns, _pd.DataFrame): - df = _pd.DataFrame({"returns_" + str(i + 1): returns[strategy_col] - for i, strategy_col in enumerate(returns.columns)}) + df = _pd.DataFrame( + { + "returns_" + str(i + 1): returns[strategy_col] + for i, strategy_col in enumerate(returns.columns) + } + ) if benchmark is not None: benchmark = _utils._prepare_benchmark(benchmark, returns.index, rf) @@ -561,27 +781,33 @@ def metrics(returns, benchmark=None, rf=0., display=True, returns, benchmark = _match_dates(returns, benchmark) df["benchmark"] = benchmark if isinstance(returns, _pd.Series): - blank = ['', ''] + blank = ["", ""] df["returns"] = returns elif isinstance(returns, _pd.DataFrame): - blank = [''] * len(returns.columns) + [''] + blank = [""] * len(returns.columns) + [""] for i, strategy_col in enumerate(returns.columns): - df["returns_" + str(i+1)] = returns[strategy_col] + df["returns_" + str(i + 1)] = returns[strategy_col] if isinstance(returns, _pd.Series): - s_start = {'returns': df['returns'].index.strftime('%Y-%m-%d')[0]} - s_end = {'returns': df['returns'].index.strftime('%Y-%m-%d')[-1]} - s_rf = {'returns': rf} + s_start = {"returns": df["returns"].index.strftime("%Y-%m-%d")[0]} + s_end = {"returns": df["returns"].index.strftime("%Y-%m-%d")[-1]} + s_rf = {"returns": rf} elif isinstance(returns, _pd.DataFrame): df_strategy_columns = [col for col in df.columns if col != "benchmark"] - s_start = {strategy_col: df[strategy_col].dropna().index.strftime('%Y-%m-%d')[0] for strategy_col in df_strategy_columns} - s_end = {strategy_col: df[strategy_col].dropna().index.strftime('%Y-%m-%d')[-1] for strategy_col in df_strategy_columns} + s_start = { + strategy_col: df[strategy_col].dropna().index.strftime("%Y-%m-%d")[0] + for strategy_col in df_strategy_columns + } + s_end = { + strategy_col: df[strategy_col].dropna().index.strftime("%Y-%m-%d")[-1] + for strategy_col in df_strategy_columns + } s_rf = {strategy_col: rf for strategy_col in df_strategy_columns} if "benchmark" in df: - s_start['benchmark'] = df['benchmark'].index.strftime('%Y-%m-%d')[0] - s_end['benchmark'] = df['benchmark'].index.strftime('%Y-%m-%d')[-1] - s_rf['benchmark'] = rf + s_start["benchmark"] = df["benchmark"].index.strftime("%Y-%m-%d")[0] + s_end["benchmark"] = df["benchmark"].index.strftime("%Y-%m-%d")[-1] + s_rf["benchmark"] = rf df = df.fillna(0) @@ -591,200 +817,295 @@ def metrics(returns, benchmark=None, rf=0., display=True, pct = 100 # return df - dd = _calc_dd(df, display=(display or "internal" in kwargs), - as_pct=kwargs.get("as_pct", False)) + dd = _calc_dd( + df, + display=(display or "internal" in kwargs), + as_pct=kwargs.get("as_pct", False), + ) metrics = _pd.DataFrame() - metrics['Start Period'] = _pd.Series(s_start) - metrics['End Period'] = _pd.Series(s_end) - metrics['Risk-Free Rate %'] = _pd.Series(s_rf)*100 - metrics['Time in Market %'] = _stats.exposure(df, prepare_returns=False) * pct + metrics["Start Period"] = _pd.Series(s_start) + metrics["End Period"] = _pd.Series(s_end) + metrics["Risk-Free Rate %"] = _pd.Series(s_rf) * 100 + metrics["Time in Market %"] = _stats.exposure(df, prepare_returns=False) * pct - metrics['~'] = blank + metrics["~"] = blank if compounded: - metrics['Cumulative Return %'] = (_stats.comp(df) * pct).map('{:,.2f}'.format) + metrics["Cumulative Return %"] = (_stats.comp(df) * pct).map("{:,.2f}".format) else: - metrics['Total Return %'] = (df.sum() * pct).map('{:,.2f}'.format) + metrics["Total Return %"] = (df.sum() * pct).map("{:,.2f}".format) - metrics['CAGR﹪%'] = _stats.cagr(df, rf, compounded) * pct + metrics["CAGR﹪%"] = _stats.cagr(df, rf, compounded) * pct - metrics['~~~~~~~~~~~~~~'] = blank + metrics["~~~~~~~~~~~~~~"] = blank - metrics['Sharpe'] = _stats.sharpe(df, rf, win_year, True) - metrics['Prob. Sharpe Ratio %'] = _stats.probabilistic_sharpe_ratio(df, rf, win_year, False) * pct - if mode.lower() == 'full': - metrics['Smart Sharpe'] = _stats.smart_sharpe(df, rf, win_year, True) + metrics["Sharpe"] = _stats.sharpe(df, rf, win_year, True) + metrics["Prob. Sharpe Ratio %"] = ( + _stats.probabilistic_sharpe_ratio(df, rf, win_year, False) * pct + ) + 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["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["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["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["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., win_year) + metrics["Omega"] = _stats.omega(df, rf, 0.0, win_year) - metrics['~~~~~~~~'] = blank - metrics['Max Drawdown %'] = blank - metrics['Longest DD Days'] = blank + metrics["~~~~~~~~"] = blank + metrics["Max Drawdown %"] = blank + metrics["Longest DD Days"] = blank - if mode.lower() == 'full': + if mode.lower() == "full": if isinstance(returns, _pd.Series): - ret_vol = _stats.volatility( - df['returns'], win_year, True, prepare_returns=False) * pct + ret_vol = ( + _stats.volatility(df["returns"], win_year, True, prepare_returns=False) + * pct + ) elif isinstance(returns, _pd.DataFrame): - ret_vol = ([_stats.volatility(df[strategy_col], win_year, True, prepare_returns=False) * pct - for strategy_col in df_strategy_columns]) + ret_vol = [ + _stats.volatility( + df[strategy_col], win_year, True, prepare_returns=False + ) + * pct + for strategy_col in df_strategy_columns + ] if "benchmark" in df: - bench_vol = _stats.volatility( - df['benchmark'], win_year, True, prepare_returns=False) * pct + bench_vol = ( + _stats.volatility( + df["benchmark"], win_year, True, prepare_returns=False + ) + * pct + ) vol_ = [ret_vol, bench_vol] if isinstance(ret_vol, list): - metrics['Volatility (ann.) %'] = list(_pd.core.common.flatten(vol_)) + metrics["Volatility (ann.) %"] = list(_pd.core.common.flatten(vol_)) else: - metrics['Volatility (ann.) %'] = vol_ + metrics["Volatility (ann.) %"] = vol_ if isinstance(returns, _pd.Series): - metrics['R^2'] = _stats.r_squared( - df['returns'], df['benchmark'], prepare_returns=False) - metrics['Information Ratio'] = _stats.information_ratio( - df['returns'], df['benchmark'], prepare_returns=False) + metrics["R^2"] = _stats.r_squared( + df["returns"], df["benchmark"], prepare_returns=False + ) + metrics["Information Ratio"] = _stats.information_ratio( + df["returns"], df["benchmark"], prepare_returns=False + ) elif isinstance(returns, _pd.DataFrame): - metrics['R^2'] = ([_stats.r_squared(df[strategy_col], df['benchmark'], prepare_returns=False).round(2) - for strategy_col in df_strategy_columns]) + ['-'] - metrics['Information Ratio'] = ([_stats.information_ratio(df[strategy_col], df['benchmark'], prepare_returns=False).round(2) - for strategy_col in df_strategy_columns]) + ['-'] + metrics["R^2"] = ( + [ + _stats.r_squared( + df[strategy_col], df["benchmark"], prepare_returns=False + ).round(2) + for strategy_col in df_strategy_columns + ] + ) + ["-"] + metrics["Information Ratio"] = ( + [ + _stats.information_ratio( + df[strategy_col], df["benchmark"], prepare_returns=False + ).round(2) + for strategy_col in df_strategy_columns + ] + ) + ["-"] else: if isinstance(returns, _pd.Series): - metrics['Volatility (ann.) %'] = [ret_vol] + metrics["Volatility (ann.) %"] = [ret_vol] elif isinstance(returns, _pd.DataFrame): - metrics['Volatility (ann.) %'] = ret_vol - - metrics['Calmar'] = _stats.calmar(df, prepare_returns=False) - metrics['Skew'] = _stats.skew(df, prepare_returns=False) - metrics['Kurtosis'] = _stats.kurtosis(df, prepare_returns=False) - - metrics['~~~~~~~~~~'] = blank - - metrics['Expected Daily %%'] = _stats.expected_return( - df, prepare_returns=False) * pct - metrics['Expected Monthly %%'] = _stats.expected_return( - df, aggregate='M', prepare_returns=False) * pct - metrics['Expected Yearly %%'] = _stats.expected_return( - df, aggregate='A', prepare_returns=False) * pct - metrics['Kelly Criterion %'] = _stats.kelly_criterion( - df, prepare_returns=False) * pct - metrics['Risk of Ruin %'] = _stats.risk_of_ruin( - df, prepare_returns=False) - - metrics['Daily Value-at-Risk %'] = -abs(_stats.var( - df, prepare_returns=False) * pct) - metrics['Expected Shortfall (cVaR) %'] = -abs(_stats.cvar( - df, prepare_returns=False) * pct) - - metrics['~~~~~~'] = blank - - if mode.lower() == 'full': - metrics['Max Consecutive Wins *int'] = _stats.consecutive_wins(df) - metrics['Max Consecutive Losses *int'] = _stats.consecutive_losses(df) - - metrics['Gain/Pain Ratio'] = _stats.gain_to_pain_ratio(df, rf) - metrics['Gain/Pain (1M)'] = _stats.gain_to_pain_ratio(df, rf, "M") + metrics["Volatility (ann.) %"] = ret_vol + + metrics["Calmar"] = _stats.calmar(df, prepare_returns=False) + metrics["Skew"] = _stats.skew(df, prepare_returns=False) + metrics["Kurtosis"] = _stats.kurtosis(df, prepare_returns=False) + + metrics["~~~~~~~~~~"] = blank + + metrics["Expected Daily %%"] = ( + _stats.expected_return(df, prepare_returns=False) * pct + ) + metrics["Expected Monthly %%"] = ( + _stats.expected_return(df, aggregate="M", prepare_returns=False) * pct + ) + metrics["Expected Yearly %%"] = ( + _stats.expected_return(df, aggregate="A", prepare_returns=False) * pct + ) + metrics["Kelly Criterion %"] = ( + _stats.kelly_criterion(df, prepare_returns=False) * pct + ) + metrics["Risk of Ruin %"] = _stats.risk_of_ruin(df, prepare_returns=False) + + metrics["Daily Value-at-Risk %"] = -abs( + _stats.var(df, prepare_returns=False) * pct + ) + metrics["Expected Shortfall (cVaR) %"] = -abs( + _stats.cvar(df, prepare_returns=False) * pct + ) + + metrics["~~~~~~"] = blank + + if mode.lower() == "full": + metrics["Max Consecutive Wins *int"] = _stats.consecutive_wins(df) + metrics["Max Consecutive Losses *int"] = _stats.consecutive_losses(df) + + 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["~~~~~~~"] = blank - metrics['Payoff Ratio'] = _stats.payoff_ratio(df, prepare_returns=False) - metrics['Profit Factor'] = _stats.profit_factor(df, prepare_returns=False) - metrics['Common Sense Ratio'] = _stats.common_sense_ratio(df, prepare_returns=False) - metrics['CPC Index'] = _stats.cpc_index(df, prepare_returns=False) - metrics['Tail Ratio'] = _stats.tail_ratio(df, prepare_returns=False) - metrics['Outlier Win Ratio'] = _stats.outlier_win_ratio(df, prepare_returns=False) - metrics['Outlier Loss Ratio'] = _stats.outlier_loss_ratio(df, prepare_returns=False) + metrics["Payoff Ratio"] = _stats.payoff_ratio(df, prepare_returns=False) + metrics["Profit Factor"] = _stats.profit_factor(df, prepare_returns=False) + metrics["Common Sense Ratio"] = _stats.common_sense_ratio(df, prepare_returns=False) + metrics["CPC Index"] = _stats.cpc_index(df, prepare_returns=False) + metrics["Tail Ratio"] = _stats.tail_ratio(df, prepare_returns=False) + metrics["Outlier Win Ratio"] = _stats.outlier_win_ratio(df, prepare_returns=False) + metrics["Outlier Loss Ratio"] = _stats.outlier_loss_ratio(df, prepare_returns=False) # returns - metrics['~~'] = blank + metrics["~~"] = blank comp_func = _stats.comp if compounded else _np.sum today = df.index[-1] # _dt.today() - metrics['MTD %'] = comp_func(df[df.index >= _dt(today.year, today.month, 1)]) * pct + metrics["MTD %"] = comp_func(df[df.index >= _dt(today.year, today.month, 1)]) * pct d = today - relativedelta(months=3) - metrics['3M %'] = comp_func(df[df.index >= d]) * pct + metrics["3M %"] = comp_func(df[df.index >= d]) * pct d = today - relativedelta(months=6) - metrics['6M %'] = comp_func(df[df.index >= d]) * pct + metrics["6M %"] = comp_func(df[df.index >= d]) * pct - metrics['YTD %'] = comp_func(df[df.index >= _dt(today.year, 1, 1)]) * pct + metrics["YTD %"] = comp_func(df[df.index >= _dt(today.year, 1, 1)]) * pct d = today - relativedelta(years=1) - metrics['1Y %'] = comp_func(df[df.index >= d]) * pct + metrics["1Y %"] = comp_func(df[df.index >= d]) * pct d = today - relativedelta(months=35) - metrics['3Y (ann.) %'] = _stats.cagr(df[df.index >= d], 0., compounded) * pct + metrics["3Y (ann.) %"] = _stats.cagr(df[df.index >= d], 0.0, compounded) * pct d = today - relativedelta(months=59) - metrics['5Y (ann.) %'] = _stats.cagr(df[df.index >= d], 0., compounded) * pct + metrics["5Y (ann.) %"] = _stats.cagr(df[df.index >= d], 0.0, compounded) * pct d = today - relativedelta(years=10) - metrics['10Y (ann.) %'] = _stats.cagr(df[df.index >= d], 0., compounded) * pct + metrics["10Y (ann.) %"] = _stats.cagr(df[df.index >= d], 0.0, compounded) * pct - metrics['All-time (ann.) %'] = _stats.cagr(df, 0., compounded) * pct + metrics["All-time (ann.) %"] = _stats.cagr(df, 0.0, compounded) * pct # best/worst - if mode.lower() == 'full': - metrics['~~~'] = blank - metrics['Best Day %'] = _stats.best(df, prepare_returns=False) * pct - metrics['Worst Day %'] = _stats.worst(df, prepare_returns=False) * pct - metrics['Best Month %'] = _stats.best(df, aggregate='M', prepare_returns=False) * pct - metrics['Worst Month %'] = _stats.worst(df, aggregate='M', prepare_returns=False) * pct - metrics['Best Year %'] = _stats.best(df, aggregate='A', prepare_returns=False) * pct - metrics['Worst Year %'] = _stats.worst(df, aggregate='A', prepare_returns=False) * pct + if mode.lower() == "full": + metrics["~~~"] = blank + metrics["Best Day %"] = _stats.best(df, prepare_returns=False) * pct + metrics["Worst Day %"] = _stats.worst(df, prepare_returns=False) * pct + metrics["Best Month %"] = ( + _stats.best(df, aggregate="M", prepare_returns=False) * pct + ) + metrics["Worst Month %"] = ( + _stats.worst(df, aggregate="M", prepare_returns=False) * pct + ) + metrics["Best Year %"] = ( + _stats.best(df, aggregate="A", prepare_returns=False) * pct + ) + metrics["Worst Year %"] = ( + _stats.worst(df, aggregate="A", prepare_returns=False) * pct + ) # dd - metrics['~~~~'] = blank + metrics["~~~~"] = blank for ix, row in dd.iterrows(): metrics[ix] = row - metrics['Recovery Factor'] = _stats.recovery_factor(df) - metrics['Ulcer Index'] = _stats.ulcer_index(df) - metrics['Serenity Index'] = _stats.serenity_index(df, rf) + metrics["Recovery Factor"] = _stats.recovery_factor(df) + metrics["Ulcer Index"] = _stats.ulcer_index(df) + metrics["Serenity Index"] = _stats.serenity_index(df, rf) # win rate - if mode.lower() == 'full': - metrics['~~~~~'] = blank - metrics['Avg. Up Month %'] = _stats.avg_win(df, aggregate='M', prepare_returns=False) * pct - metrics['Avg. Down Month %'] = _stats.avg_loss(df, aggregate='M', prepare_returns=False) * pct - metrics['Win Days %%'] = _stats.win_rate(df, prepare_returns=False) * pct - metrics['Win Month %%'] = _stats.win_rate(df, aggregate='M', prepare_returns=False) * pct - metrics['Win Quarter %%'] = _stats.win_rate(df, aggregate='Q', prepare_returns=False) * pct - metrics['Win Year %%'] = _stats.win_rate(df, aggregate='A', prepare_returns=False) * pct + if mode.lower() == "full": + metrics["~~~~~"] = blank + metrics["Avg. Up Month %"] = ( + _stats.avg_win(df, aggregate="M", prepare_returns=False) * pct + ) + metrics["Avg. Down Month %"] = ( + _stats.avg_loss(df, aggregate="M", prepare_returns=False) * pct + ) + metrics["Win Days %%"] = _stats.win_rate(df, prepare_returns=False) * pct + metrics["Win Month %%"] = ( + _stats.win_rate(df, aggregate="M", prepare_returns=False) * pct + ) + metrics["Win Quarter %%"] = ( + _stats.win_rate(df, aggregate="Q", prepare_returns=False) * pct + ) + metrics["Win Year %%"] = ( + _stats.win_rate(df, aggregate="A", prepare_returns=False) * pct + ) if "benchmark" in df: - metrics['~~~~~~~~~~~~'] = blank + metrics["~~~~~~~~~~~~"] = blank if isinstance(returns, _pd.Series): - greeks = _stats.greeks(df['returns'], df['benchmark'], win_year, prepare_returns=False) - metrics['Beta'] = [str(round(greeks['beta'], 2)), '-'] - metrics['Alpha'] = [str(round(greeks['alpha'], 2)), '-'] - metrics['Correlation'] = [str(round(df['benchmark'].corr(df['returns']) * pct, 2))+'%', '-'] - metrics['Treynor Ratio'] = [str(round(_stats.treynor_ratio(df['returns'], df['benchmark'], win_year, rf)*pct, 2))+'%', '-'] + greeks = _stats.greeks( + df["returns"], df["benchmark"], win_year, prepare_returns=False + ) + metrics["Beta"] = [str(round(greeks["beta"], 2)), "-"] + metrics["Alpha"] = [str(round(greeks["alpha"], 2)), "-"] + metrics["Correlation"] = [ + str(round(df["benchmark"].corr(df["returns"]) * pct, 2)) + "%", + "-", + ] + metrics["Treynor Ratio"] = [ + str( + round( + _stats.treynor_ratio( + df["returns"], df["benchmark"], win_year, rf + ) + * pct, + 2, + ) + ) + + "%", + "-", + ] elif isinstance(returns, _pd.DataFrame): - greeks = ([_stats.greeks(df[strategy_col], df['benchmark'], win_year, prepare_returns=False) - for strategy_col in df_strategy_columns]) - metrics['Beta'] = [str(round(g['beta'], 2)) for g in greeks] + ['-'] - metrics['Alpha'] = [str(round(g['alpha'], 2)) for g in greeks] + ['-'] - metrics['Correlation'] = ([str(round(df['benchmark'].corr(df[strategy_col]) * pct, 2)) + '%' - for strategy_col in df_strategy_columns]) + ['-'] - metrics['Treynor Ratio'] = ([str(round(_stats.treynor_ratio(df[strategy_col], df['benchmark'], win_year, rf) * pct, 2)) + '%' - for strategy_col in df_strategy_columns]) + ['-'] + greeks = [ + _stats.greeks( + df[strategy_col], + df["benchmark"], + win_year, + prepare_returns=False, + ) + for strategy_col in df_strategy_columns + ] + metrics["Beta"] = [str(round(g["beta"], 2)) for g in greeks] + ["-"] + metrics["Alpha"] = [str(round(g["alpha"], 2)) for g in greeks] + ["-"] + metrics["Correlation"] = ( + [ + str(round(df["benchmark"].corr(df[strategy_col]) * pct, 2)) + + "%" + for strategy_col in df_strategy_columns + ] + ) + ["-"] + metrics["Treynor Ratio"] = ( + [ + str( + round( + _stats.treynor_ratio( + df[strategy_col], df["benchmark"], win_year, rf + ) + * pct, + 2, + ) + ) + + "%" + for strategy_col in df_strategy_columns + ] + ) + ["-"] # prepare for display for col in metrics.columns: @@ -795,32 +1116,31 @@ def metrics(returns, benchmark=None, rf=0., display=True, except Exception: pass if (display or "internal" in kwargs) and "*int" in col: - metrics[col] = metrics[col].str.replace('.0', '', regex=False) + metrics[col] = metrics[col].str.replace(".0", "", regex=False) metrics.rename({col: col.replace("*int", "")}, axis=1, inplace=True) if (display or "internal" in kwargs) and "%" in col: - metrics[col] = metrics[col] + '%' + metrics[col] = metrics[col] + "%" try: - metrics['Longest DD Days'] = _pd.to_numeric( - metrics['Longest DD Days']).astype('int') - metrics['Avg. Drawdown Days'] = _pd.to_numeric( - metrics['Avg. Drawdown Days']).astype('int') + metrics["Longest DD Days"] = _pd.to_numeric(metrics["Longest DD Days"]).astype( + "int" + ) + metrics["Avg. Drawdown Days"] = _pd.to_numeric( + metrics["Avg. Drawdown Days"] + ).astype("int") if display or "internal" in kwargs: - metrics['Longest DD Days'] = metrics['Longest DD Days'].astype(str) - metrics['Avg. Drawdown Days'] = metrics['Avg. Drawdown Days' - ].astype(str) + metrics["Longest DD Days"] = metrics["Longest DD Days"].astype(str) + metrics["Avg. Drawdown Days"] = metrics["Avg. Drawdown Days"].astype(str) except Exception: - metrics['Longest DD Days'] = '-' - metrics['Avg. Drawdown Days'] = '-' + metrics["Longest DD Days"] = "-" + metrics["Avg. Drawdown Days"] = "-" if display or "internal" in kwargs: - metrics['Longest DD Days'] = '-' - metrics['Avg. Drawdown Days'] = '-' + metrics["Longest DD Days"] = "-" + metrics["Avg. Drawdown Days"] = "-" - metrics.columns = [ - col if '~' not in col else '' for col in metrics.columns] - metrics.columns = [ - col[:-1] if '%' in col else col for col in metrics.columns] + metrics.columns = [col if "~" not in col else "" for col in metrics.columns] + metrics.columns = [col[:-1] if "%" in col else col for col in metrics.columns] metrics = metrics.T if "benchmark" in df: @@ -836,38 +1156,66 @@ def metrics(returns, benchmark=None, rf=0., display=True, metrics.columns = [strategy_colname] # cleanups - metrics.replace([-0, '-0'], 0, inplace=True) - metrics.replace([_np.nan, -_np.nan, _np.inf, -_np.inf, - '-nan%', 'nan%', '-nan', 'nan', - '-inf%', 'inf%', '-inf', 'inf'], '-', inplace=True) + metrics.replace([-0, "-0"], 0, inplace=True) + metrics.replace( + [ + _np.nan, + -_np.nan, + _np.inf, + -_np.inf, + "-nan%", + "nan%", + "-nan", + "nan", + "-inf%", + "inf%", + "-inf", + "inf", + ], + "-", + inplace=True, + ) # move benchmark to be the first column always if present if "benchmark" in df: - metrics = metrics[[benchmark_colname] + [col for col in metrics.columns if col != benchmark_colname]] + metrics = metrics[ + [benchmark_colname] + + [col for col in metrics.columns if col != benchmark_colname] + ] if display: - print(_tabulate(metrics, headers="keys", tablefmt='simple')) + print(_tabulate(metrics, headers="keys", tablefmt="simple")) return None if not sep: - metrics = metrics[metrics.index != ''] + metrics = metrics[metrics.index != ""] # remove spaces from column names metrics = metrics.T - metrics.columns = [c.replace(' %', '').replace(' *int', '').strip() for c in metrics.columns] + metrics.columns = [ + c.replace(" %", "").replace(" *int", "").strip() for c in metrics.columns + ] metrics = metrics.T return metrics -def plots(returns, benchmark=None, grayscale=False, - figsize=(8, 5), mode='basic', compounded=True, - periods_per_year=252, prepare_returns=True, - match_dates=True, **kwargs): +def plots( + returns, + benchmark=None, + grayscale=False, + figsize=(8, 5), + mode="basic", + compounded=True, + periods_per_year=252, + prepare_returns=True, + match_dates=True, + **kwargs, +): benchmark_colname = kwargs.get("benchmark_title", "Benchmark") strategy_colname = kwargs.get("strategy_title", "Strategy") - active = kwargs.get('active', 'False') + active = kwargs.get("active", "False") if isinstance(returns, _pd.DataFrame): if len(returns.columns) > 1: @@ -887,23 +1235,41 @@ def plots(returns, benchmark=None, grayscale=False, elif isinstance(returns, _pd.DataFrame): returns.columns = strategy_colname - if mode.lower() != 'full': - _plots.snapshot(returns, grayscale=grayscale, - figsize=(figsize[0], figsize[0]), - show=True, mode=("comp" if compounded else "sum"), - benchmark_title=benchmark_colname, strategy_title=strategy_colname) + if mode.lower() != "full": + _plots.snapshot( + returns, + grayscale=grayscale, + figsize=(figsize[0], figsize[0]), + show=True, + mode=("comp" if compounded else "sum"), + benchmark_title=benchmark_colname, + strategy_title=strategy_colname, + ) if isinstance(returns, _pd.Series): - _plots.monthly_heatmap(returns, benchmark, grayscale=grayscale, - figsize=(figsize[0], figsize[0]*.5), - show=True, ylabel=False, - compounded=compounded, active=active) + _plots.monthly_heatmap( + returns, + benchmark, + grayscale=grayscale, + figsize=(figsize[0], figsize[0] * 0.5), + show=True, + ylabel=False, + compounded=compounded, + active=active, + ) elif isinstance(returns, _pd.DataFrame): for col in returns.columns: - _plots.monthly_heatmap(returns[col].dropna(), benchmark, grayscale=grayscale, - figsize=(figsize[0], figsize[0] * .5), - show=True, ylabel=False, returns_label=col, - compounded=compounded, active=active) + _plots.monthly_heatmap( + returns[col].dropna(), + benchmark, + grayscale=grayscale, + figsize=(figsize[0], figsize[0] * 0.5), + show=True, + ylabel=False, + returns_label=col, + compounded=compounded, + active=active, + ) return @@ -916,100 +1282,192 @@ def plots(returns, benchmark=None, grayscale=False, if match_dates is True: returns, benchmark = _match_dates(returns, benchmark) - _plots.returns(returns, benchmark, grayscale=grayscale, - figsize=(figsize[0], figsize[0]*.6), - show=True, ylabel=False, - prepare_returns=False) + _plots.returns( + returns, + benchmark, + grayscale=grayscale, + figsize=(figsize[0], figsize[0] * 0.6), + show=True, + ylabel=False, + prepare_returns=False, + ) - _plots.log_returns(returns, benchmark, grayscale=grayscale, - figsize=(figsize[0], figsize[0]*.5), - show=True, ylabel=False, - prepare_returns=False) + _plots.log_returns( + returns, + benchmark, + grayscale=grayscale, + figsize=(figsize[0], figsize[0] * 0.5), + show=True, + ylabel=False, + prepare_returns=False, + ) if benchmark is not None: - _plots.returns(returns, benchmark, match_volatility=True, - grayscale=grayscale, - figsize=(figsize[0], figsize[0]*.5), - show=True, ylabel=False, - prepare_returns=False) - - _plots.yearly_returns(returns, benchmark, - grayscale=grayscale, - figsize=(figsize[0], figsize[0]*.5), - show=True, ylabel=False, - prepare_returns=False) - - _plots.histogram(returns, benchmark, grayscale=grayscale, - figsize=(figsize[0], figsize[0]*.5), - show=True, ylabel=False, - prepare_returns=False) - - small_fig_size = (figsize[0], figsize[0]*.35) - if len(returns.columns) > 1: - small_fig_size = (figsize[0], figsize[0]*(.33*(len(returns.columns)*.66))) + _plots.returns( + returns, + benchmark, + match_volatility=True, + grayscale=grayscale, + figsize=(figsize[0], figsize[0] * 0.5), + show=True, + ylabel=False, + prepare_returns=False, + ) + + _plots.yearly_returns( + returns, + benchmark, + grayscale=grayscale, + figsize=(figsize[0], figsize[0] * 0.5), + show=True, + ylabel=False, + prepare_returns=False, + ) - _plots.daily_returns(returns, benchmark, grayscale=grayscale, - figsize=small_fig_size, - show=True, ylabel=False, - prepare_returns=False, active=active) + _plots.histogram( + returns, + benchmark, + grayscale=grayscale, + figsize=(figsize[0], figsize[0] * 0.5), + show=True, + ylabel=False, + prepare_returns=False, + ) - if benchmark is not None: - _plots.rolling_beta(returns, benchmark, grayscale=grayscale, - window1=win_half_year, window2=win_year, - figsize=small_fig_size, - show=True, ylabel=False, - prepare_returns=False) + small_fig_size = (figsize[0], figsize[0] * 0.35) + if len(returns.columns) > 1: + small_fig_size = ( + figsize[0], + figsize[0] * (0.33 * (len(returns.columns) * 0.66)), + ) + + _plots.daily_returns( + returns, + benchmark, + grayscale=grayscale, + figsize=small_fig_size, + show=True, + ylabel=False, + prepare_returns=False, + active=active, + ) - _plots.rolling_volatility(returns, benchmark, grayscale=grayscale, - figsize=small_fig_size, show=True, ylabel=False, - period=win_half_year) + if benchmark is not None: + _plots.rolling_beta( + returns, + benchmark, + grayscale=grayscale, + window1=win_half_year, + window2=win_year, + figsize=small_fig_size, + show=True, + ylabel=False, + prepare_returns=False, + ) + + _plots.rolling_volatility( + returns, + benchmark, + grayscale=grayscale, + figsize=small_fig_size, + show=True, + ylabel=False, + period=win_half_year, + ) - _plots.rolling_sharpe(returns, grayscale=grayscale, - figsize=small_fig_size, - show=True, ylabel=False, period=win_half_year) + _plots.rolling_sharpe( + returns, + grayscale=grayscale, + figsize=small_fig_size, + show=True, + ylabel=False, + period=win_half_year, + ) - _plots.rolling_sortino(returns, grayscale=grayscale, - figsize=small_fig_size, - show=True, ylabel=False, period=win_half_year) + _plots.rolling_sortino( + returns, + grayscale=grayscale, + figsize=small_fig_size, + show=True, + ylabel=False, + period=win_half_year, + ) if isinstance(returns, _pd.Series): - _plots.drawdowns_periods(returns, grayscale=grayscale, - figsize=(figsize[0], figsize[0]*.5), - show=True, ylabel=False, - prepare_returns=False) + _plots.drawdowns_periods( + returns, + grayscale=grayscale, + figsize=(figsize[0], figsize[0] * 0.5), + show=True, + ylabel=False, + prepare_returns=False, + ) elif isinstance(returns, _pd.DataFrame): for col in returns.columns: - _plots.drawdowns_periods(returns[col], grayscale=grayscale, - figsize=(figsize[0], figsize[0] * .5), - show=True, ylabel=False, title=col, - prepare_returns=False) - - _plots.drawdown(returns, grayscale=grayscale, - figsize=(figsize[0], figsize[0]*.4), - show=True, ylabel=False) + _plots.drawdowns_periods( + returns[col], + grayscale=grayscale, + figsize=(figsize[0], figsize[0] * 0.5), + show=True, + ylabel=False, + title=col, + prepare_returns=False, + ) + + _plots.drawdown( + returns, + grayscale=grayscale, + figsize=(figsize[0], figsize[0] * 0.4), + show=True, + ylabel=False, + ) if isinstance(returns, _pd.Series): - _plots.monthly_heatmap(returns, benchmark, grayscale=grayscale, - figsize=(figsize[0], figsize[0]*.5), returns_label=returns.name, - show=True, ylabel=False, active=active) + _plots.monthly_heatmap( + returns, + benchmark, + grayscale=grayscale, + figsize=(figsize[0], figsize[0] * 0.5), + returns_label=returns.name, + show=True, + ylabel=False, + active=active, + ) elif isinstance(returns, _pd.DataFrame): for col in returns.columns: - _plots.monthly_heatmap(returns[col], benchmark, grayscale=grayscale, - figsize=(figsize[0], figsize[0] * .5), - show=True, ylabel=False, returns_label=col, - compounded=compounded, active=active) + _plots.monthly_heatmap( + returns[col], + benchmark, + grayscale=grayscale, + figsize=(figsize[0], figsize[0] * 0.5), + show=True, + ylabel=False, + returns_label=col, + compounded=compounded, + active=active, + ) if isinstance(returns, _pd.Series): - _plots.distribution(returns, grayscale=grayscale, - figsize=(figsize[0], figsize[0] * .5), - show=True, title=returns.name, ylabel=False, - prepare_returns=False) + _plots.distribution( + returns, + grayscale=grayscale, + figsize=(figsize[0], figsize[0] * 0.5), + show=True, + title=returns.name, + ylabel=False, + prepare_returns=False, + ) elif isinstance(returns, _pd.DataFrame): for col in returns.columns: - _plots.distribution(returns[col], grayscale=grayscale, - figsize=(figsize[0], figsize[0] * .5), - show=True, title=col, ylabel=False, - prepare_returns=False) + _plots.distribution( + returns[col], + grayscale=grayscale, + figsize=(figsize[0], figsize[0] * 0.5), + show=True, + title=col, + ylabel=False, + prepare_returns=False, + ) def _calc_dd(df, display=True, as_pct=False): @@ -1020,96 +1478,127 @@ def _calc_dd(df, display=True, as_pct=False): return _pd.DataFrame() if "returns" in dd_info: - ret_dd = dd_info['returns'] + ret_dd = dd_info["returns"] # to match multiple columns like returns_1, returns_2, ... - elif any(dd_info.columns.get_level_values(0).str.contains('returns')) \ - and dd_info.columns.get_level_values(0).nunique() > 1: - ret_dd = dd_info.loc[:, dd_info.columns.get_level_values(0).str.contains('returns')] + elif ( + any(dd_info.columns.get_level_values(0).str.contains("returns")) + and dd_info.columns.get_level_values(0).nunique() > 1 + ): + ret_dd = dd_info.loc[ + :, dd_info.columns.get_level_values(0).str.contains("returns") + ] else: ret_dd = dd_info - if any(ret_dd.columns.get_level_values(0).str.contains('returns')) and \ - ret_dd.columns.get_level_values(0).nunique() > 1: + if ( + any(ret_dd.columns.get_level_values(0).str.contains("returns")) + and ret_dd.columns.get_level_values(0).nunique() > 1 + ): dd_stats = { col: { - 'Max Drawdown %': ret_dd[col].sort_values( - by='max drawdown', ascending=True - )['max drawdown'].values[0] / 100, - 'Longest DD Days': str(_np.round(ret_dd[col].sort_values( - by='days', ascending=False)['days'].values[0])), - 'Avg. Drawdown %': ret_dd[col]['max drawdown'].mean() / 100, - 'Avg. Drawdown Days': str(_np.round(ret_dd[col]['days'].mean())) + "Max Drawdown %": ret_dd[col] + .sort_values(by="max drawdown", ascending=True)["max drawdown"] + .values[0] + / 100, + "Longest DD Days": str( + _np.round( + ret_dd[col] + .sort_values(by="days", ascending=False)["days"] + .values[0] + ) + ), + "Avg. Drawdown %": ret_dd[col]["max drawdown"].mean() / 100, + "Avg. Drawdown Days": str(_np.round(ret_dd[col]["days"].mean())), } for col in ret_dd.columns.get_level_values(0) } else: dd_stats = { - 'returns': { - 'Max Drawdown %': ret_dd.sort_values( - by='max drawdown', ascending=True - )['max drawdown'].values[0] / 100, - 'Longest DD Days': str(_np.round(ret_dd.sort_values( - by='days', ascending=False)['days'].values[0])), - 'Avg. Drawdown %': ret_dd['max drawdown'].mean() / 100, - 'Avg. Drawdown Days': str(_np.round(ret_dd['days'].mean())) + "returns": { + "Max Drawdown %": ret_dd.sort_values(by="max drawdown", ascending=True)[ + "max drawdown" + ].values[0] + / 100, + "Longest DD Days": str( + _np.round( + ret_dd.sort_values(by="days", ascending=False)["days"].values[0] + ) + ), + "Avg. Drawdown %": ret_dd["max drawdown"].mean() / 100, + "Avg. Drawdown Days": str(_np.round(ret_dd["days"].mean())), } } if "benchmark" in df and (dd_info.columns, _pd.MultiIndex): - bench_dd = dd_info['benchmark'].sort_values(by='max drawdown') - dd_stats['benchmark'] = { - 'Max Drawdown %': bench_dd.sort_values( - by='max drawdown', ascending=True - )['max drawdown'].values[0] / 100, - 'Longest DD Days': str(_np.round(bench_dd.sort_values( - by='days', ascending=False)['days'].values[0])), - 'Avg. Drawdown %': bench_dd['max drawdown'].mean() / 100, - 'Avg. Drawdown Days': str(_np.round(bench_dd['days'].mean())) + bench_dd = dd_info["benchmark"].sort_values(by="max drawdown") + dd_stats["benchmark"] = { + "Max Drawdown %": bench_dd.sort_values(by="max drawdown", ascending=True)[ + "max drawdown" + ].values[0] + / 100, + "Longest DD Days": str( + _np.round( + bench_dd.sort_values(by="days", ascending=False)["days"].values[0] + ) + ), + "Avg. Drawdown %": bench_dd["max drawdown"].mean() / 100, + "Avg. Drawdown Days": str(_np.round(bench_dd["days"].mean())), } # pct multiplier pct = 100 if display or as_pct else 1 dd_stats = _pd.DataFrame(dd_stats).T - dd_stats['Max Drawdown %'] = dd_stats['Max Drawdown %'].astype(float) * pct - dd_stats['Avg. Drawdown %'] = dd_stats['Avg. Drawdown %'].astype(float) * pct + dd_stats["Max Drawdown %"] = dd_stats["Max Drawdown %"].astype(float) * pct + dd_stats["Avg. Drawdown %"] = dd_stats["Avg. Drawdown %"].astype(float) * pct return dd_stats.T def _html_table(obj, showindex="default"): - obj = _tabulate(obj, headers="keys", tablefmt='html', - floatfmt=".2f", showindex=showindex) - obj = obj.replace(' style="text-align: right;"', '') - obj = obj.replace(' style="text-align: left;"', '') - obj = obj.replace(' style="text-align: center;"', '') - obj = _regex.sub(' +', '', obj) - obj = _regex.sub(' +', '', obj) - obj = _regex.sub(' +', '', obj) - obj = _regex.sub(' +', '', obj) + obj = _tabulate( + obj, headers="keys", tablefmt="html", floatfmt=".2f", showindex=showindex + ) + obj = obj.replace(' style="text-align: right;"', "") + obj = obj.replace(' style="text-align: left;"', "") + obj = obj.replace(' style="text-align: center;"', "") + obj = _regex.sub(" +", "", obj) + obj = _regex.sub(" +", "", obj) + obj = _regex.sub(" +", "", obj) + obj = _regex.sub(" +", "", obj) return obj def _download_html(html, filename="quantstats-tearsheet.html"): - jscode = _regex.sub(' +', ' ', """""".replace('\n', '')) - jscode = jscode.replace('{{html}}', _regex.sub( - ' +', ' ', html.replace('\n', ''))) + a.click();""".replace( + "\n", "" + ), + ) + jscode = jscode.replace("{{html}}", _regex.sub(" +", " ", html.replace("\n", ""))) if _utils._in_notebook(): - iDisplay(iHTML(jscode.replace('{{filename}}', filename))) + iDisplay(iHTML(jscode.replace("{{filename}}", filename))) def _open_html(html): - jscode = _regex.sub(' +', ' ', """""".replace('\n', '')) - jscode = jscode.replace('{{html}}', _regex.sub( - ' +', ' ', html.replace('\n', ''))) + """.replace( + "\n", "" + ), + ) + jscode = jscode.replace("{{html}}", _regex.sub(" +", " ", html.replace("\n", ""))) if _utils._in_notebook(): iDisplay(iHTML(jscode)) @@ -1119,13 +1608,15 @@ def _embed_figure(figfiles, figfmt): embed_string = "\n" for figfile in figfiles: figbytes = figfile.getvalue() - if figfmt == 'svg': + if figfmt == "svg": return figbytes.decode() data_uri = _b64encode(figbytes).decode() - embed_string.join(''.format(figfmt, data_uri)) + embed_string.join( + ''.format(figfmt, data_uri) + ) else: figbytes = figfiles.getvalue() - if figfmt == 'svg': + if figfmt == "svg": return figbytes.decode() data_uri = _b64encode(figbytes).decode() embed_string = ''.format(figfmt, data_uri) diff --git a/quantstats/stats.py b/quantstats/stats.py index 7e81158e..27346f61 100644 --- a/quantstats/stats.py +++ b/quantstats/stats.py @@ -22,19 +22,18 @@ import pandas as _pd import numpy as _np from math import ceil as _ceil, sqrt as _sqrt -from scipy.stats import ( - norm as _norm, linregress as _linregress -) +from scipy.stats import norm as _norm, linregress as _linregress from . import utils as _utils # ======== STATS ======== + def pct_rank(prices, window=60): """Rank prices by window""" rank = _utils.multi_shift(prices, window).T.rank(pct=True).T - return rank.iloc[:, 0] * 100. + return rank.iloc[:, 0] * 100.0 def compsum(returns): @@ -60,12 +59,14 @@ def get_outliers(data): } if isinstance(returns, _pd.DataFrame): - warn("Pandas DataFrame was passed (Series expected). " - "Only first column will be used.") + warn( + "Pandas DataFrame was passed (Series expected). " + "Only first column will be used." + ) returns = returns.copy() returns.columns = map(str.lower, returns.columns) - if len(returns.columns) > 1 and 'close' in returns.columns: - returns = returns['close'] + if len(returns.columns) > 1 and "close" in returns.columns: + returns = returns["close"] else: returns = returns[returns.columns[0]] @@ -77,15 +78,14 @@ def get_outliers(data): return { "Daily": get_outliers(daily), - "Weekly": get_outliers(daily.resample('W-MON').apply(apply_fnc)), - "Monthly": get_outliers(daily.resample('M').apply(apply_fnc)), - "Quarterly": get_outliers(daily.resample('Q').apply(apply_fnc)), - "Yearly": get_outliers(daily.resample('A').apply(apply_fnc)) + "Weekly": get_outliers(daily.resample("W-MON").apply(apply_fnc)), + "Monthly": get_outliers(daily.resample("M").apply(apply_fnc)), + "Quarterly": get_outliers(daily.resample("Q").apply(apply_fnc)), + "Yearly": get_outliers(daily.resample("A").apply(apply_fnc)), } -def expected_return(returns, aggregate=None, compounded=True, - prepare_returns=True): +def expected_return(returns, aggregate=None, compounded=True, prepare_returns=True): """ Returns the expected return for a given period by calculating the geometric holding period return @@ -106,12 +106,12 @@ def ghpr(retruns, aggregate=None, compounded=True): return expected_return(retruns, aggregate, compounded) -def outliers(returns, quantile=.95): +def outliers(returns, quantile=0.95): """Returns series of outliers""" - return returns[returns > returns.quantile(quantile)].dropna(how='all') + return returns[returns > returns.quantile(quantile)].dropna(how="all") -def remove_outliers(returns, quantile=.95): +def remove_outliers(returns, quantile=0.95): """Returns series of returns without the outliers""" return returns[returns < returns.quantile(quantile)] @@ -130,8 +130,7 @@ def worst(returns, aggregate=None, compounded=True, prepare_returns=True): return _utils.aggregate_returns(returns, aggregate, compounded).min() -def consecutive_wins(returns, aggregate=None, compounded=True, - prepare_returns=True): +def consecutive_wins(returns, aggregate=None, compounded=True, prepare_returns=True): """Returns the maximum consecutive wins by day/month/week/quarter/year""" if prepare_returns: returns = _utils._prepare_returns(returns) @@ -139,8 +138,7 @@ def consecutive_wins(returns, aggregate=None, compounded=True, return _utils._count_consecutive(returns).max() -def consecutive_losses(returns, aggregate=None, compounded=True, - prepare_returns=True): +def consecutive_losses(returns, aggregate=None, compounded=True, prepare_returns=True): """ Returns the maximum consecutive losses by day/month/week/quarter/year @@ -170,11 +168,12 @@ def _exposure(ret): def win_rate(returns, aggregate=None, compounded=True, prepare_returns=True): """Calculates the win ratio for a period""" + def _win_rate(series): try: return len(series[series > 0]) / len(series[series != 0]) except Exception: - return 0. + return 0.0 if prepare_returns: returns = _utils._prepare_returns(returns) @@ -235,8 +234,9 @@ def volatility(returns, periods=252, annualize=True, prepare_returns=True): return std -def rolling_volatility(returns, rolling_period=126, periods_per_year=252, - prepare_returns=True): +def rolling_volatility( + returns, rolling_period=126, periods_per_year=252, prepare_returns=True +): if prepare_returns: returns = _utils._prepare_returns(returns, rolling_period) @@ -262,13 +262,14 @@ def autocorr_penalty(returns, prepare_returns=False): # 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)] + corr = [((num - x) / num) * coef**x for x in range(1, num)] return _np.sqrt(1 + 2 * _np.sum(corr)) # ======= METRICS ======= -def sharpe(returns, rf=0., periods=252, annualize=True, smart=False): + +def sharpe(returns, rf=0.0, periods=252, annualize=True, smart=False): """ Calculates the sharpe ratio of access returns @@ -283,7 +284,7 @@ def sharpe(returns, rf=0., periods=252, annualize=True, smart=False): * smart: return smart sharpe ratio """ if rf != 0 and periods is None: - raise Exception('Must provide periods if rf != 0') + raise Exception("Must provide periods if rf != 0") returns = _utils._prepare_returns(returns, rf, periods) divisor = returns.std(ddof=1) @@ -293,32 +294,34 @@ def sharpe(returns, rf=0., periods=252, annualize=True, smart=False): res = returns.mean() / divisor if annualize: - return res * _np.sqrt( - 1 if periods is None else periods) + return res * _np.sqrt(1 if periods is None else periods) return res -def smart_sharpe(returns, rf=0., periods=252, annualize=True): +def smart_sharpe(returns, rf=0.0, periods=252, annualize=True): return sharpe(returns, rf, periods, annualize, True) -def rolling_sharpe(returns, rf=0., rolling_period=126, - annualize=True, periods_per_year=252, - prepare_returns=True): +def rolling_sharpe( + returns, + rf=0.0, + rolling_period=126, + annualize=True, + periods_per_year=252, + prepare_returns=True, +): if rf != 0 and rolling_period is None: - raise Exception('Must provide periods if rf != 0') + raise Exception("Must provide periods if rf != 0") if prepare_returns: returns = _utils._prepare_returns(returns, rf, rolling_period) - res = returns.rolling(rolling_period).mean() / \ - returns.rolling(rolling_period).std() + res = returns.rolling(rolling_period).mean() / returns.rolling(rolling_period).std() if annualize: - res = res * _np.sqrt( - 1 if periods_per_year is None else periods_per_year) + res = res * _np.sqrt(1 if periods_per_year is None else periods_per_year) return res @@ -333,7 +336,7 @@ def sortino(returns, rf=0, periods=252, annualize=True, smart=False): http://www.redrockcapital.com/Sortino__A__Sharper__Ratio_Red_Rock_Capital.pdf """ if rf != 0 and periods is None: - raise Exception('Must provide periods if rf != 0') + raise Exception("Must provide periods if rf != 0") returns = _utils._prepare_returns(returns, rf, periods) @@ -346,8 +349,7 @@ def sortino(returns, rf=0, periods=252, annualize=True, smart=False): res = returns.mean() / downside if annualize: - return res * _np.sqrt( - 1 if periods is None else periods) + return res * _np.sqrt(1 if periods is None else periods) return res @@ -356,21 +358,25 @@ def smart_sortino(returns, rf=0, periods=252, annualize=True): return sortino(returns, rf, periods, annualize, True) -def rolling_sortino(returns, rf=0, rolling_period=126, annualize=True, - periods_per_year=252, **kwargs): +def rolling_sortino( + returns, rf=0, rolling_period=126, annualize=True, periods_per_year=252, **kwargs +): if rf != 0 and rolling_period is None: - raise Exception('Must provide periods if rf != 0') + raise Exception("Must provide periods if rf != 0") if kwargs.get("prepare_returns", True): returns = _utils._prepare_returns(returns, rf, rolling_period) - downside = returns.rolling(rolling_period).apply( - lambda x: (x.values[x.values < 0]**2).sum()) / rolling_period + downside = ( + returns.rolling(rolling_period).apply( + lambda x: (x.values[x.values < 0] ** 2).sum() + ) + / rolling_period + ) res = returns.rolling(rolling_period).mean() / _np.sqrt(downside) if annualize: - res = res * _np.sqrt( - 1 if periods_per_year is None else periods_per_year) + res = res * _np.sqrt(1 if periods_per_year is None else periods_per_year) return res @@ -380,55 +386,77 @@ def adjusted_sortino(returns, rf=0, periods=252, annualize=True, smart=False): direct comparisons to the Sharpe. See here for more info: https://archive.is/wip/2rwFW """ - data = sortino( - returns, rf, periods=periods, annualize=annualize, smart=smart) + data = sortino(returns, rf, periods=periods, annualize=annualize, smart=smart) return data / _sqrt(2) -def probabilistic_ratio(series, rf=0., base="sharpe", periods=252, annualize=False, smart=False): +def probabilistic_ratio( + series, rf=0.0, base="sharpe", periods=252, annualize=False, smart=False +): if base.lower() == "sharpe": base = sharpe(series, periods=periods, annualize=False, smart=smart) elif base.lower() == "sortino": base = sortino(series, periods=periods, annualize=False, smart=smart) elif base.lower() == "adjusted_sortino": - base = adjusted_sortino(series, periods=periods, - annualize=False, smart=smart) + base = adjusted_sortino(series, periods=periods, annualize=False, smart=smart) else: raise Exception( - '`metric` must be either `sharpe`, `sortino`, or `adjusted_sortino`') + "`metric` must be either `sharpe`, `sortino`, or `adjusted_sortino`" + ) skew_no = skew(series, prepare_returns=False) kurtosis_no = kurtosis(series, prepare_returns=False) n = len(series) - sigma_sr = _np.sqrt((1 + (0.5 * base ** 2) - (skew_no * base) + - (((kurtosis_no - 3) / 4) * base ** 2)) / (n - 1)) + sigma_sr = _np.sqrt( + ( + 1 + + (0.5 * base**2) + - (skew_no * base) + + (((kurtosis_no - 3) / 4) * base**2) + ) + / (n - 1) + ) ratio = (base - rf) / sigma_sr psr = _norm.cdf(ratio) if annualize: - return psr * (252 ** 0.5) + return psr * (252**0.5) return psr -def probabilistic_sharpe_ratio(series, rf=0., periods=252, annualize=False, smart=False): - return probabilistic_ratio(series, rf, base="sharpe", periods=periods, - annualize=annualize, smart=smart) +def probabilistic_sharpe_ratio( + series, rf=0.0, periods=252, annualize=False, smart=False +): + return probabilistic_ratio( + series, rf, base="sharpe", periods=periods, annualize=annualize, smart=smart + ) -def probabilistic_sortino_ratio(series, rf=0., periods=252, annualize=False, smart=False): - return probabilistic_ratio(series, rf, base="sortino", periods=periods, - annualize=annualize, smart=smart) +def probabilistic_sortino_ratio( + series, rf=0.0, periods=252, annualize=False, smart=False +): + return probabilistic_ratio( + series, rf, base="sortino", periods=periods, annualize=annualize, smart=smart + ) -def probabilistic_adjusted_sortino_ratio(series, rf=0., periods=252, annualize=False, smart=False): - return probabilistic_ratio(series, rf, base="adjusted_sortino", periods=periods, - annualize=annualize, smart=smart) +def probabilistic_adjusted_sortino_ratio( + series, rf=0.0, periods=252, annualize=False, smart=False +): + return probabilistic_ratio( + series, + rf, + base="adjusted_sortino", + periods=periods, + annualize=annualize, + smart=smart, + ) -def treynor_ratio(returns, benchmark, periods=252., rf=0.): +def treynor_ratio(returns, benchmark, periods=252.0, rf=0.0): """ Calculates the Treynor ratio @@ -440,7 +468,7 @@ def treynor_ratio(returns, benchmark, periods=252., rf=0.): if isinstance(returns, _pd.DataFrame): returns = returns[returns.columns[0]] - beta = greeks(returns, benchmark, periods=periods).to_dict().get('beta', 0) + beta = greeks(returns, benchmark, periods=periods).to_dict().get("beta", 0) if beta == 0: return 0 return (comp(returns) - rf) / beta @@ -462,12 +490,11 @@ def omega(returns, rf=0.0, required_return=0.0, periods=252): if periods == 1: return_threshold = required_return else: - return_threshold = (1 + required_return) ** (1. / periods) - 1 + return_threshold = (1 + required_return) ** (1.0 / periods) - 1 returns_less_thresh = returns - return_threshold numer = returns_less_thresh[returns_less_thresh > 0.0].sum().values[0] - denom = -1.0 * \ - returns_less_thresh[returns_less_thresh < 0.0].sum().values[0] + denom = -1.0 * returns_less_thresh[returns_less_thresh < 0.0].sum().values[0] if denom > 0.0: return numer / denom @@ -485,7 +512,7 @@ def gain_to_pain_ratio(returns, rf=0, resolution="D"): return returns.sum() / downside -def cagr(returns, rf=0., periods=252 ,compounded=True): +def cagr(returns, rf=0.0, periods=252, compounded=True): """ Calculates the communicative annualized growth return (CAGR%) of access returns @@ -510,7 +537,7 @@ def cagr(returns, rf=0., periods=252 ,compounded=True): return res -def rar(returns, rf=0.): +def rar(returns, rf=0.0): """ Calculates the risk-adjusted return of access returns (CAGR / exposure. takes time into account.) @@ -562,7 +589,7 @@ def ulcer_performance_index(returns, rf=0): Calculates the ulcer index score (downside risk measurment) """ - return (comp(returns)-rf) / ulcer_index(returns) + return (comp(returns) - rf) / ulcer_index(returns) def upi(returns, rf=0): @@ -576,8 +603,8 @@ def serenity_index(returns, rf=0): (https://www.keyquant.com/Download/GetFile?Filename=%5CPublications%5CKeyQuant_WhitePaper_APT_Part1.pdf) """ dd = to_drawdown_series(returns) - pitfall = - cvar(dd) / returns.std() - return (comp(returns)-rf) / (ulcer_index(returns) * pitfall) + pitfall = -cvar(dd) / returns.std() + return (comp(returns) - rf) / (ulcer_index(returns) * pitfall) def risk_of_ruin(returns, prepare_returns=True): @@ -607,9 +634,9 @@ def value_at_risk(returns, sigma=1, confidence=0.95, prepare_returns=True): sigma *= returns.std() if confidence > 1: - confidence = confidence/100 + confidence = confidence / 100 - return _norm.ppf(1-confidence, mu, sigma) + return _norm.ppf(1 - confidence, mu, sigma) def var(returns, sigma=1, confidence=0.95, prepare_returns=True): @@ -617,8 +644,7 @@ def var(returns, sigma=1, confidence=0.95, prepare_returns=True): return value_at_risk(returns, sigma, confidence, prepare_returns) -def conditional_value_at_risk(returns, sigma=1, confidence=0.95, - prepare_returns=True): +def conditional_value_at_risk(returns, sigma=1, confidence=0.95, prepare_returns=True): """ Calculats the conditional daily value-at-risk (aka expected shortfall) quantifies the amount of tail risk an investment @@ -632,8 +658,7 @@ def conditional_value_at_risk(returns, sigma=1, confidence=0.95, def cvar(returns, sigma=1, confidence=0.95, prepare_returns=True): """Shorthand for conditional_value_at_risk()""" - return conditional_value_at_risk( - returns, sigma, confidence, prepare_returns) + return conditional_value_at_risk(returns, sigma, confidence, prepare_returns) def expected_shortfall(returns, sigma=1, confidence=0.95): @@ -648,7 +673,7 @@ def tail_ratio(returns, cutoff=0.95, prepare_returns=True): """ if prepare_returns: returns = _utils._prepare_returns(returns) - return abs(returns.quantile(cutoff) / returns.quantile(1-cutoff)) + return abs(returns.quantile(cutoff) / returns.quantile(1 - cutoff)) def payoff_ratio(returns, prepare_returns=True): @@ -675,7 +700,7 @@ def profit_ratio(returns, prepare_returns=True): try: return win_ratio / loss_ratio except Exception: - return 0. + return 0.0 def profit_factor(returns, prepare_returns=True): @@ -692,8 +717,7 @@ def cpc_index(returns, prepare_returns=True): """ if prepare_returns: returns = _utils._prepare_returns(returns) - return profit_factor(returns) * win_rate(returns) * \ - win_loss_ratio(returns) + return profit_factor(returns) * win_rate(returns) * win_loss_ratio(returns) def common_sense_ratio(returns, prepare_returns=True): @@ -703,7 +727,7 @@ def common_sense_ratio(returns, prepare_returns=True): return profit_factor(returns) * tail_ratio(returns) -def outlier_win_ratio(returns, quantile=.99, prepare_returns=True): +def outlier_win_ratio(returns, quantile=0.99, prepare_returns=True): """ Calculates the outlier winners ratio 99th percentile of returns / mean positive return @@ -713,7 +737,7 @@ def outlier_win_ratio(returns, quantile=.99, prepare_returns=True): return returns.quantile(quantile).mean() / returns[returns >= 0].mean() -def outlier_loss_ratio(returns, quantile=.01, prepare_returns=True): +def outlier_loss_ratio(returns, quantile=0.01, prepare_returns=True): """ Calculates the outlier losers ratio 1st percentile of returns / mean negative return @@ -751,7 +775,7 @@ def max_drawdown(prices): def to_drawdown_series(returns): """Convert returns series to drawdown series""" prices = _utils._prepare_prices(returns) - dd = prices / _np.maximum.accumulate(prices) - 1. + dd = prices / _np.maximum.accumulate(prices) - 1.0 return dd.replace([_np.inf, -_np.inf, -0], 0) @@ -761,6 +785,7 @@ def drawdown_details(drawdown): duration, max drawdown and max dd for 99% of the dd period for every drawdown period """ + def _drawdown_details(drawdown): # mark no drawdown no_dd = drawdown == 0 @@ -777,8 +802,16 @@ def _drawdown_details(drawdown): # no drawdown :) if not starts: return _pd.DataFrame( - index=[], columns=('start', 'valley', 'end', 'days', - 'max drawdown', '99% max drawdown')) + index=[], + columns=( + "start", + "valley", + "end", + "days", + "max drawdown", + "99% max drawdown", + ), + ) # drawdown series begins in a drawdown if ends and starts[0] > ends[0]: @@ -791,23 +824,37 @@ def _drawdown_details(drawdown): # build dataframe from results data = [] for i, _ in enumerate(starts): - dd = drawdown[starts[i]:ends[i]] - clean_dd = -remove_outliers(-dd, .99) - data.append((starts[i], dd.idxmin(), ends[i], - (ends[i] - starts[i]).days+1, - dd.min() * 100, clean_dd.min() * 100)) - - df = _pd.DataFrame(data=data, - columns=('start', 'valley', 'end', 'days', - 'max drawdown', - '99% max drawdown')) - df['days'] = df['days'].astype(int) - df['max drawdown'] = df['max drawdown'].astype(float) - df['99% max drawdown'] = df['99% max drawdown'].astype(float) - - df['start'] = df['start'].dt.strftime('%Y-%m-%d') - df['end'] = df['end'].dt.strftime('%Y-%m-%d') - df['valley'] = df['valley'].dt.strftime('%Y-%m-%d') + dd = drawdown[starts[i] : ends[i]] + clean_dd = -remove_outliers(-dd, 0.99) + data.append( + ( + starts[i], + dd.idxmin(), + ends[i], + (ends[i] - starts[i]).days + 1, + dd.min() * 100, + clean_dd.min() * 100, + ) + ) + + df = _pd.DataFrame( + data=data, + columns=( + "start", + "valley", + "end", + "days", + "max drawdown", + "99% max drawdown", + ), + ) + df["days"] = df["days"].astype(int) + df["max drawdown"] = df["max drawdown"].astype(float) + df["99% max drawdown"] = df["99% max drawdown"].astype(float) + + df["start"] = df["start"].dt.strftime("%Y-%m-%d") + df["end"] = df["end"].dt.strftime("%Y-%m-%d") + df["valley"] = df["valley"].dt.strftime("%Y-%m-%d") return df @@ -837,13 +884,15 @@ def kelly_criterion(returns, prepare_returns=True): # ==== VS. BENCHMARK ==== + 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( - returns, _utils._prepare_benchmark(benchmark, returns.index)) + returns, _utils._prepare_benchmark(benchmark, returns.index) + ) return r_val**2 @@ -864,7 +913,7 @@ def information_ratio(returns, benchmark, prepare_returns=True): return diff_rets.mean() / diff_rets.std() -def greeks(returns, benchmark, periods=252., prepare_returns=True): +def greeks(returns, benchmark, periods=252.0, prepare_returns=True): """Calculates alpha and beta of the portfolio""" # ---------------------------- # data cleanup @@ -881,37 +930,44 @@ def greeks(returns, benchmark, periods=252., prepare_returns=True): alpha = returns.mean() - beta * benchmark.mean() alpha = alpha * periods - return _pd.Series({ - "beta": beta, - "alpha": alpha, - # "vol": _np.sqrt(matrix[0, 0]) * _np.sqrt(periods) - }).fillna(0) + return _pd.Series( + { + "beta": beta, + "alpha": alpha, + # "vol": _np.sqrt(matrix[0, 0]) * _np.sqrt(periods) + } + ).fillna(0) def rolling_greeks(returns, benchmark, periods=252, prepare_returns=True): """Calculates rolling alpha and beta of the portfolio""" if prepare_returns: returns = _utils._prepare_returns(returns) - df = _pd.DataFrame(data={ - "returns": returns, - "benchmark": _utils._prepare_benchmark(benchmark, returns.index) - }) + df = _pd.DataFrame( + data={ + "returns": returns, + "benchmark": _utils._prepare_benchmark(benchmark, returns.index), + } + ) df = df.fillna(0) - corr = df.rolling(int(periods)).corr().unstack()['returns']['benchmark'] + corr = df.rolling(int(periods)).corr().unstack()["returns"]["benchmark"] std = df.rolling(int(periods)).std() - beta = corr * std['returns'] / std['benchmark'] + beta = corr * std["returns"] / std["benchmark"] - alpha = df['returns'].mean() - beta * df['benchmark'].mean() + alpha = df["returns"].mean() - beta * df["benchmark"].mean() # alpha = alpha * periods - return _pd.DataFrame(index=returns.index, data={ - "beta": beta, - "alpha": alpha - }) + return _pd.DataFrame(index=returns.index, data={"beta": beta, "alpha": alpha}) -def compare(returns, benchmark, aggregate=None, compounded=True, - round_vals=None, prepare_returns=True): +def compare( + returns, + benchmark, + aggregate=None, + compounded=True, + round_vals=None, + prepare_returns=True, +): """ Compare returns to benchmark on a day/week/month/quarter/year basis @@ -921,19 +977,28 @@ def compare(returns, benchmark, aggregate=None, compounded=True, benchmark = _utils._prepare_benchmark(benchmark, returns.index) if isinstance(returns, _pd.Series): - data = _pd.DataFrame(data={ - 'Benchmark': _utils.aggregate_returns( - benchmark, aggregate, compounded) * 100, - 'Returns': _utils.aggregate_returns( - returns, aggregate, compounded) * 100 - }) - - data['Multiplier'] = data['Returns'] / data['Benchmark'] - data['Won'] = _np.where(data['Returns'] >= data['Benchmark'], '+', '-') + data = _pd.DataFrame( + data={ + "Benchmark": _utils.aggregate_returns(benchmark, aggregate, compounded) + * 100, + "Returns": _utils.aggregate_returns(returns, aggregate, compounded) + * 100, + } + ) + + data["Multiplier"] = data["Returns"] / data["Benchmark"] + data["Won"] = _np.where(data["Returns"] >= data["Benchmark"], "+", "-") elif isinstance(returns, _pd.DataFrame): - bench = {'Benchmark': _utils.aggregate_returns(benchmark, aggregate, compounded) * 100} - strategy = {'Returns_' + str(i): _utils.aggregate_returns(returns[col], aggregate, compounded) * 100 - for i, col in enumerate(returns.columns)} + bench = { + "Benchmark": _utils.aggregate_returns(benchmark, aggregate, compounded) + * 100 + } + strategy = { + "Returns_" + + str(i): _utils.aggregate_returns(returns[col], aggregate, compounded) + * 100 + for i, col in enumerate(returns.columns) + } data = _pd.DataFrame(data={**bench, **strategy}) if round_vals is not None: @@ -945,12 +1010,14 @@ def compare(returns, benchmark, aggregate=None, compounded=True, def monthly_returns(returns, eoy=True, compounded=True, prepare_returns=True): """Calculates monthly returns""" if isinstance(returns, _pd.DataFrame): - warn("Pandas DataFrame was passed (Series expected). " - "Only first column will be used.") + warn( + "Pandas DataFrame was passed (Series expected). " + "Only first column will be used." + ) returns = returns.copy() returns.columns = map(str.lower, returns.columns) - if len(returns.columns) > 1 and 'close' in returns.columns: - returns = returns['close'] + if len(returns.columns) > 1 and "close" in returns.columns: + returns = returns["close"] else: returns = returns[returns.columns[0]] @@ -959,35 +1026,59 @@ def monthly_returns(returns, eoy=True, compounded=True, prepare_returns=True): original_returns = returns.copy() returns = _pd.DataFrame( - _utils.group_returns(returns, - returns.index.strftime('%Y-%m-01'), - compounded)) + _utils.group_returns(returns, returns.index.strftime("%Y-%m-01"), compounded) + ) - returns.columns = ['Returns'] + returns.columns = ["Returns"] returns.index = _pd.to_datetime(returns.index) # get returnsframe - returns['Year'] = returns.index.strftime('%Y') - returns['Month'] = returns.index.strftime('%b') + returns["Year"] = returns.index.strftime("%Y") + returns["Month"] = returns.index.strftime("%b") # make pivot table - returns = returns.pivot(index='Year', columns='Month', values='Returns').fillna(0) + returns = returns.pivot(index="Year", columns="Month", values="Returns").fillna(0) # handle missing months - for month in ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']: + for month in [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]: if month not in returns.columns: returns.loc[:, month] = 0 # order columns by month - returns = returns[['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']] + returns = returns[ + [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ] + ] if eoy: - returns['eoy'] = _utils.group_returns( - original_returns, - original_returns.index.year, - compounded=compounded).values + returns["eoy"] = _utils.group_returns( + original_returns, original_returns.index.year, compounded=compounded + ).values returns.columns = map(lambda x: str(x).upper(), returns.columns) returns.index.name = None diff --git a/quantstats/utils.py b/quantstats/utils.py index e788b391..51eff8db 100644 --- a/quantstats/utils.py +++ b/quantstats/utils.py @@ -27,22 +27,19 @@ def _mtd(df): - return df[df.index >= _dt.datetime.now( - ).strftime('%Y-%m-01')] + return df[df.index >= _dt.datetime.now().strftime("%Y-%m-01")] def _qtd(df): date = _dt.datetime.now() for q in [1, 4, 7, 10]: if date.month <= q: - return df[df.index >= _dt.datetime( - date.year, q, 1).strftime('%Y-%m-01')] - return df[df.index >= date.strftime('%Y-%m-01')] + return df[df.index >= _dt.datetime(date.year, q, 1).strftime("%Y-%m-01")] + return df[df.index >= date.strftime("%Y-%m-01")] def _ytd(df): - return df[df.index >= _dt.datetime.now( - ).strftime('%Y-01-01')] + return df[df.index >= _dt.datetime.now().strftime("%Y-01-01")] def _pandas_date(df, dates): @@ -68,42 +65,42 @@ def multi_shift(df, shift=3): return _pd.concat(dfs, 1, sort=True) -def to_returns(prices, rf=0.): +def to_returns(prices, rf=0.0): """Calculates the simple arithmetic returns of a price series""" return _prepare_returns(prices, rf) def to_prices(returns, base=1e5): """Converts returns series to price data""" - returns = returns.copy().fillna(0).replace( - [_np.inf, -_np.inf], float('NaN')) + returns = returns.copy().fillna(0).replace([_np.inf, -_np.inf], float("NaN")) return base + base * _stats.compsum(returns) -def log_returns(returns, rf=0., nperiods=None): +def log_returns(returns, rf=0.0, nperiods=None): """Shorthand for to_log_returns""" return to_log_returns(returns, rf, nperiods) -def to_log_returns(returns, rf=0., nperiods=None): +def to_log_returns(returns, rf=0.0, nperiods=None): """Converts returns series to log returns""" returns = _prepare_returns(returns, rf, nperiods) try: - return _np.log(returns+1).replace([_np.inf, -_np.inf], float('NaN')) + return _np.log(returns + 1).replace([_np.inf, -_np.inf], float("NaN")) except Exception: - return 0. + return 0.0 def exponential_stdev(returns, window=30, is_halflife=False): """Returns series representing exponential volatility of returns""" returns = _prepare_returns(returns) halflife = window if is_halflife else None - return returns.ewm(com=None, span=window, - halflife=halflife, min_periods=window).std() + return returns.ewm( + com=None, span=window, halflife=halflife, min_periods=window + ).std() -def rebase(prices, base=100.): +def rebase(prices, base=100.0): """ Rebase all series to a given intial base. This makes comparing/plotting different series together easier. @@ -126,33 +123,32 @@ def group_returns(returns, groupby, compounded=False): def aggregate_returns(returns, period=None, compounded=True): """Aggregates returns based on date periods""" - if period is None or 'day' in period: + if period is None or "day" in period: return returns index = returns.index - if 'month' in period: + if "month" in period: return group_returns(returns, index.month, compounded=compounded) - if 'quarter' in period: + if "quarter" in period: return group_returns(returns, index.quarter, compounded=compounded) - if period == "A" or any(x in period for x in ['year', 'eoy', 'yoy']): + if period == "A" or any(x in period for x in ["year", "eoy", "yoy"]): return group_returns(returns, index.year, compounded=compounded) - if 'week' in period: + if "week" in period: return group_returns(returns, index.week, compounded=compounded) - if 'eow' in period or period == "W": - return group_returns(returns, [index.year, index.week], - compounded=compounded) + if "eow" in period or period == "W": + return group_returns(returns, [index.year, index.week], compounded=compounded) - if 'eom' in period or period == "M": - return group_returns(returns, [index.year, index.month], - compounded=compounded) + if "eom" in period or period == "M": + return group_returns(returns, [index.year, index.month], compounded=compounded) - if 'eoq' in period or period == "Q": - return group_returns(returns, [index.year, index.quarter], - compounded=compounded) + if "eoq" in period or period == "Q": + return group_returns( + returns, [index.year, index.quarter], compounded=compounded + ) if not isinstance(period, str): return group_returns(returns, period, compounded) @@ -181,12 +177,12 @@ def to_excess_returns(returns, rf, nperiods=None): if nperiods is not None: # deannualize - rf = _np.power(1 + rf, 1. / nperiods) - 1. + rf = _np.power(1 + rf, 1.0 / nperiods) - 1.0 return returns - rf -def _prepare_prices(data, base=1.): +def _prepare_prices(data, base=1.0): """Converts return data into prices + cleanup""" data = data.copy() if isinstance(data, _pd.DataFrame): @@ -200,13 +196,12 @@ def _prepare_prices(data, base=1.): data = to_prices(data, base) if isinstance(data, (_pd.DataFrame, _pd.Series)): - data = data.fillna(0).replace( - [_np.inf, -_np.inf], float('NaN')) + data = data.fillna(0).replace([_np.inf, -_np.inf], float("NaN")) return data -def _prepare_returns(data, rf=0., nperiods=None): +def _prepare_returns(data, rf=0.0, nperiods=None): """Converts price data into returns + cleanup""" data = data.copy() function = inspect.stack()[1][3] @@ -218,15 +213,16 @@ def _prepare_returns(data, rf=0., nperiods=None): data = data.pct_change() # cleanup data - data = data.replace([_np.inf, -_np.inf], float('NaN')) + data = data.replace([_np.inf, -_np.inf], float("NaN")) if isinstance(data, (_pd.DataFrame, _pd.Series)): - data = data.fillna(0).replace( - [_np.inf, -_np.inf], float('NaN')) - unnecessary_function_calls = ['_prepare_benchmark', - 'cagr', - 'gain_to_pain_ratio', - 'rolling_volatility'] + data = data.fillna(0).replace([_np.inf, -_np.inf], float("NaN")) + unnecessary_function_calls = [ + "_prepare_benchmark", + "cagr", + "gain_to_pain_ratio", + "rolling_volatility", + ] if function not in unnecessary_function_calls: if rf > 0: @@ -243,11 +239,10 @@ def download_returns(ticker, period="max", proxy=None): params["start"] = period[0] else: params["period"] = period - return _yf.download(**params)['Close'].pct_change() + return _yf.download(**params)["Close"].pct_change() -def _prepare_benchmark(benchmark=None, period="max", rf=0., - prepare_returns=True): +def _prepare_benchmark(benchmark=None, period="max", rf=0.0, prepare_returns=True): """ Fetch benchmark if ticker is provided, and pass through _prepare_returns() @@ -263,14 +258,17 @@ def _prepare_benchmark(benchmark=None, period="max", rf=0., elif isinstance(benchmark, _pd.DataFrame): benchmark = benchmark[benchmark.columns[0]].copy() - if isinstance(period, _pd.DatetimeIndex) \ - and set(period) != set(benchmark.index): + if isinstance(period, _pd.DatetimeIndex) and set(period) != set(benchmark.index): # Adjust Benchmark to Strategy frequency benchmark_prices = to_prices(benchmark, base=1) - new_index = _pd.date_range(start=period[0], end=period[-1], freq='D') - benchmark = benchmark_prices.reindex(new_index, method='bfill') \ - .reindex(period).pct_change().fillna(0) + new_index = _pd.date_range(start=period[0], end=period[-1], freq="D") + benchmark = ( + benchmark_prices.reindex(new_index, method="bfill") + .reindex(period) + .pct_change() + .fillna(0) + ) benchmark = benchmark[benchmark.index.isin(period)] benchmark.index = benchmark.index.tz_localize(None) @@ -283,7 +281,7 @@ def _prepare_benchmark(benchmark=None, period="max", rf=0., def _round_to_closest(val, res, decimals=None): """Round to closest resolution""" if decimals is None and "." in str(res): - decimals = len(str(res).split('.')[1]) + decimals = len(str(res).split(".")[1]) return round(round(val / res) * res, decimals) @@ -296,12 +294,12 @@ def _in_notebook(matplotlib_inline=False): """Identify enviroment (notebook, terminal, etc)""" try: shell = get_ipython().__class__.__name__ - if shell == 'ZMQInteractiveShell': + if shell == "ZMQInteractiveShell": # Jupyter notebook or qtconsole if matplotlib_inline: get_ipython().magic("matplotlib inline") return True - if shell == 'TerminalInteractiveShell': + if shell == "TerminalInteractiveShell": # Terminal running IPython return False # Other type (?) @@ -313,9 +311,9 @@ def _in_notebook(matplotlib_inline=False): def _count_consecutive(data): """Counts consecutive data (like cumsum() with reset on zeroes)""" + def _count(data): - return data * (data.groupby( - (data != data.shift(1)).cumsum()).cumcount() + 1) + return data * (data.groupby((data != data.shift(1)).cumsum()).cumcount() + 1) if isinstance(data, _pd.DataFrame): for col in data.columns: @@ -329,7 +327,9 @@ def _score_str(val): return ("" if "-" in val else "+") + str(val) -def make_index(ticker_weights, rebalance="1M", period="max", returns=None, match_dates=False): +def make_index( + ticker_weights, rebalance="1M", period="max", returns=None, match_dates=False +): """ Makes an index out of the given tickers and weights. Optionally you can pass a dataframe with the returns. @@ -365,7 +365,7 @@ def make_index(ticker_weights, rebalance="1M", period="max", returns=None, match index = _pd.DataFrame(portfolio).dropna() if match_dates: - index=index[max(index.ne(0).idxmax()):] + index = index[max(index.ne(0).idxmax()) :] # no rebalance? if rebalance is None: @@ -377,30 +377,30 @@ def make_index(ticker_weights, rebalance="1M", period="max", returns=None, match # rebalance marker rbdf = index.resample(rebalance).first() - rbdf['break'] = rbdf.index.strftime('%s') + rbdf["break"] = rbdf.index.strftime("%s") # index returns with rebalance markers - index = _pd.concat([index, rbdf['break']], axis=1) + index = _pd.concat([index, rbdf["break"]], axis=1) # mark first day day - index['first_day'] = _pd.isna(index['break']) & ~_pd.isna(index['break'].shift(1)) - index.loc[index.index[0], 'first_day'] = True + index["first_day"] = _pd.isna(index["break"]) & ~_pd.isna(index["break"].shift(1)) + index.loc[index.index[0], "first_day"] = True # multiply first day of each rebalance period by the weight for ticker, weight in ticker_weights.items(): index[ticker] = _np.where( - index['first_day'], weight * index[ticker], index[ticker]) + index["first_day"], weight * index[ticker], index[ticker] + ) # drop first marker - index.drop(columns=['first_day'], inplace=True) + index.drop(columns=["first_day"], inplace=True) # drop when all are NaN index.dropna(how="all", inplace=True) return index[index.index <= last_day].sum(axis=1) -def make_portfolio(returns, start_balance=1e5, - mode="comp", round_to=None): +def make_portfolio(returns, start_balance=1e5, mode="comp", round_to=None): """Calculates compounded value of portfolio""" returns = _prepare_returns(returns) @@ -410,13 +410,13 @@ def make_portfolio(returns, start_balance=1e5, p1 = to_prices(returns, start_balance) else: # fixed amount every day - comp_rev = (start_balance + start_balance * - returns.shift(1)).fillna(start_balance) * returns + comp_rev = (start_balance + start_balance * returns.shift(1)).fillna( + start_balance + ) * returns p1 = start_balance + comp_rev.cumsum() # add day before with starting balance - p0 = _pd.Series(data=start_balance, - index=p1.index + _pd.Timedelta(days=-1))[:1] + p0 = _pd.Series(data=start_balance, index=p1.index + _pd.Timedelta(days=-1))[:1] portfolio = _pd.concat([p0, p1])