diff --git a/quantstats/_plotting/core.py b/quantstats/_plotting/core.py index 818aca55..74a759e6 100644 --- a/quantstats/_plotting/core.py +++ b/quantstats/_plotting/core.py @@ -50,9 +50,10 @@ 'ytick.color': '#666666' }) -_FLATUI_COLORS = ["#fedd78", "#348dc1", "#af4b64", - "#4fa487", "#9b59b6", "#808080"] -_GRAYSCALE_COLORS = ['silver', '#222222', 'gray'] * 3 +_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): @@ -108,11 +109,11 @@ def plot_returns_bars(returns, benchmark=None, ax.spines['left'].set_visible(False) # use a more precise date string for the x axis locations in the toolbar - fig.suptitle(title+"\n", y=.99, fontweight="bold", fontname=fontname, + fig.suptitle(title, y=.94, fontweight="bold", fontname=fontname, fontsize=14, color="black") if subtitle: - ax.set_title("\n%s - %s " % ( + ax.set_title("%s - %s \n" % ( df.index.date[:1][0].strftime('%Y'), df.index.date[-1:][0].strftime('%Y') ), fontsize=12, color='gray') @@ -151,7 +152,7 @@ def plot_returns_bars(returns, benchmark=None, ax.axhline(0, ls="--", lw=1, color="#000000", zorder=2) # if isinstance(benchmark, _pd.Series) or hline: - ax.legend(fontsize=12) + ax.legend(fontsize=11) _plt.yscale("symlog" if log_scale else "linear") @@ -163,6 +164,9 @@ def plot_returns_bars(returns, benchmark=None, ax.yaxis.set_major_formatter(_FuncFormatter(format_pct_axis)) + if benchmark is None and len(_pd.DataFrame(returns).columns) == 1: + ax.get_legend().remove() + try: _plt.subplots_adjust(hspace=0, bottom=0, top=1) except Exception: @@ -238,11 +242,11 @@ def plot_timeseries(returns, benchmark=None, ax.spines['bottom'].set_visible(False) ax.spines['left'].set_visible(False) - fig.suptitle(title+"\n", y=.99, fontweight="bold", fontname=fontname, + fig.suptitle(title, y=.94, fontweight="bold", fontname=fontname, fontsize=14, color="black") if subtitle: - ax.set_title("\n%s - %s " % ( + 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') @@ -258,8 +262,8 @@ def plot_timeseries(returns, benchmark=None, 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 col in returns.columns: - ax.plot(returns[col], lw=lw, label=col, alpha=alpha) + for i, col in enumerate(returns.columns): + ax.plot(returns[col], lw=lw, label=col, alpha=alpha, color=colors[i+1]) if fill: if isinstance(returns, _pd.Series): @@ -287,8 +291,7 @@ def plot_timeseries(returns, benchmark=None, color='white' if grayscale else 'black', zorder=2) # if isinstance(benchmark, _pd.Series) or hline is not None: - # ax.legend(fontsize=12) - ax.legend(fontsize=12) + ax.legend(fontsize=11) _plt.yscale("symlog" if log_scale else "linear") @@ -303,6 +306,9 @@ def plot_timeseries(returns, benchmark=None, fontweight='bold', fontsize=12, color="black") ax.yaxis.set_label_coords(-.1, .5) + if benchmark is None and len(_pd.DataFrame(returns).columns) == 1: + ax.get_legend().remove() + try: _plt.subplots_adjust(hspace=0, bottom=0, top=1) except Exception: @@ -336,9 +342,11 @@ def plot_histogram(returns, benchmark, resample="M", bins=20, ylabel=True, subtitle=True, compounded=True, savefig=None, show=True): - colors = ['#348dc1', '#003366', 'red'] - if grayscale: - colors = ['silver', 'gray', 'black'] + # colors = ['#348dc1', '#003366', 'red'] + # if grayscale: + # colors = ['silver', 'gray', 'black'] + + colors, _, _ = _get_colors(grayscale) apply_fnc = _stats.comp if compounded else _np.sum if benchmark is not None: @@ -348,17 +356,19 @@ def plot_histogram(returns, benchmark, resample="M", bins=20, 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) - fig.suptitle(title+"\n", y=.99, fontweight="bold", fontname=fontname, + fig.suptitle(title, y=.94, fontweight="bold", fontname=fontname, fontsize=14, color="black") if subtitle: - ax.set_title("\n%s - %s " % ( + ax.set_title("%s - %s \n" % ( returns.index.date[:1][0].strftime('%Y'), returns.index.date[-1:][0].strftime('%Y') ), fontsize=12, color='gray') @@ -366,40 +376,60 @@ def plot_histogram(returns, benchmark, resample="M", bins=20, fig.set_facecolor('white') ax.set_facecolor('white') - # Why do we need average? - # ax.axvline(returns.mean(), ls="--", lw=1.5, - # zorder=2, label="Average") + if isinstance(returns, _pd.DataFrame) and len(returns.columns) == 1: + returns = returns[returns.columns[0]] + + 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] + 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': 'Portfolio', 0: 'Returns'}) + .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': 'Portfolio', 0: 'Returns'}) + .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) - _sns.histplot(data=combined_returns, x='Returns', - bins=bins, alpha=0.4, kde=kde, - stat="density", hue='Portfolio', - ax=ax) else: if isinstance(returns, _pd.Series): combined_returns = returns.copy() - _sns.histplot(data=combined_returns, bins=bins, - alpha=0.4, kde=kde, - stat="density", - ax=ax) - ax.legend(fontsize=12) + 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) + elif isinstance(returns, _pd.DataFrame): combined_returns = returns.stack().reset_index() \ - .rename(columns={'level_1': 'Portfolio', 0: 'Returns'}) - _sns.histplot(data=combined_returns, x='Returns', - bins=bins, alpha=0.4, kde=kde, - stat="density", hue='Portfolio', - ax=ax) + .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) + + # 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") + + # _plt.setp(x.get_legend().get_texts(), fontsize=11) ax.xaxis.set_major_formatter(_plt.FuncFormatter( lambda x, loc: "{:,}%".format(int(x*100)))) @@ -408,10 +438,9 @@ def plot_histogram(returns, benchmark, resample="M", bins=20, # ax.axvline(0, lw=1, color="#000000", zorder=2) ax.set_xlabel('') - if ylabel: - ax.set_ylabel("Occurrences", fontname=fontname, - fontweight='bold', fontsize=12, color="black") - ax.yaxis.set_label_coords(-.1, .5) + ax.set_ylabel("Occurrences", fontname=fontname, + fontweight='bold', fontsize=12, color="black") + ax.yaxis.set_label_coords(-.1, .5) # fig.autofmt_xdate() @@ -473,9 +502,8 @@ def plot_rolling_stats(returns, benchmark=None, title="", elif isinstance(returns, _pd.DataFrame): col_names = ['Benchmark', returns_label] df = df[list(_pd.core.common.flatten(col_names))].dropna() - for col in returns_label: - ax.plot(df[col], lw=lw, - label=col) + 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) else: @@ -485,20 +513,19 @@ def plot_rolling_stats(returns, benchmark=None, title="", label=returns.name, color=colors[1]) elif isinstance(returns, _pd.DataFrame): df = df[returns_label].dropna() - for col in returns_label: - ax.plot(df[col], lw=lw, - label=col) + for i, col in enumerate(returns_label): + 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+"\n", y=.99, fontweight="bold", fontname=fontname, + fig.suptitle(title, y=.94, fontweight="bold", fontname=fontname, fontsize=14, color="black") if subtitle: - ax.set_title("\n%s - %s " % ( + 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') @@ -519,7 +546,10 @@ def plot_rolling_stats(returns, benchmark=None, title="", ax.yaxis.set_major_formatter(_FormatStrFormatter('%.2f')) - ax.legend(fontsize=12) + ax.legend(fontsize=11) + + if benchmark is None and len(_pd.DataFrame(returns).columns) == 1: + ax.get_legend().remove() try: _plt.subplots_adjust(hspace=0, bottom=0, top=1) @@ -562,15 +592,16 @@ def plot_rolling_beta(returns, benchmark, ax.spines['bottom'].set_visible(False) ax.spines['left'].set_visible(False) - fig.suptitle(title+"\n", y=.99, fontweight="bold", fontname=fontname, + fig.suptitle(title, y=.94, fontweight="bold", fontname=fontname, fontsize=14, color="black") if subtitle: - ax.set_title("\n%s - %s " % ( + 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) ax.plot(beta, lw=lw, label=window1_label, color=colors[1]) @@ -578,9 +609,12 @@ def plot_rolling_beta(returns, benchmark, 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})") + ax.plot(b, lw=lw, label=name + " " + f"({window1_label})", color=colors[i]) + i += 1 + i = 1 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) @@ -588,9 +622,8 @@ def plot_rolling_beta(returns, benchmark, 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.8) - # ax.plot(_stats.rolling_greeks(returns, benchmark, window2)['beta'], - # lw=lw, label=window2_label, color="gray", alpha=0.8) + 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()]) @@ -616,7 +649,10 @@ def plot_rolling_beta(returns, benchmark, fontweight='bold', fontsize=12, color="black") ax.yaxis.set_label_coords(-.1, .5) - ax.legend(fontsize=12) + ax.legend(fontsize=11) + if benchmark is None and len(_pd.DataFrame(returns).columns) == 1: + ax.get_legend().remove() + try: _plt.subplots_adjust(hspace=0, bottom=0, top=1) except Exception: @@ -665,11 +701,11 @@ def plot_longest_drawdowns(returns, periods=5, lw=1.5, ax.spines['bottom'].set_visible(False) ax.spines['left'].set_visible(False) - fig.suptitle(f"{title} - Worst %.0f Drawdown Periods\n" % - periods, y=.99, fontweight="bold", fontname=fontname, + fig.suptitle(f"{title} - Worst %.0f Drawdown Periods" % + periods, y=.94, fontweight="bold", fontname=fontname, fontsize=14, color="black") if subtitle: - ax.set_title("\n%s - %s " % ( + 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') @@ -767,12 +803,16 @@ def plot_distribution(returns, figsize=(10, 6), ax.spines['bottom'].set_visible(False) ax.spines['left'].set_visible(False) - fig.suptitle(f"{title} - Return Quantiles\n", y=.99, + if title: + title = f"{title} - Return Quantiles" + else: + title = "Return Quantiles" + fig.suptitle(title, y=.94, fontweight="bold", fontname=fontname, fontsize=14, color="black") if subtitle: - ax.set_title("\n%s - %s " % ( + 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') @@ -780,7 +820,14 @@ def plot_distribution(returns, figsize=(10, 6), fig.set_facecolor('white') ax.set_facecolor('white') - port.boxplot(ax=ax)#, colo=tuple(colors[:5])) + _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)))) diff --git a/quantstats/_plotting/wrappers.py b/quantstats/_plotting/wrappers.py index 889b6f50..6f595220 100644 --- a/quantstats/_plotting/wrappers.py +++ b/quantstats/_plotting/wrappers.py @@ -64,16 +64,23 @@ def snapshot(returns, grayscale=False, figsize=(10, 8), mode="comp", subtitle=True, savefig=None, show=True, log_scale=False, **kwargs): - strategy_colname = kwargs.get("strategy_title", "Strategy") + strategy_colname = kwargs.get("strategy_col", "Strategy") + multi_column = False if isinstance(returns, _pd.Series): returns.name = strategy_colname elif isinstance(returns, _pd.DataFrame): + if len(returns.columns) > 1: + if strategy_colname in returns.columns: + returns = returns[strategy_colname] + else: + multi_column = True + returns = returns.mean(axis=1) + title = title + " (daily equal-weighted*)" returns.columns = strategy_colname colors = _GRAYSCALE_COLORS if grayscale else _FLATUI_COLORS - - returns = _utils.make_portfolio(returns, 1, mode).pct_change().fillna(0) + returns = _utils.make_portfolio(returns.dropna(), 1, mode).pct_change().fillna(0) if figsize is None: size = list(_plt.gcf().get_size_inches()) @@ -82,20 +89,27 @@ def snapshot(returns, grayscale=False, figsize=(10, 8), fig, axes = _plt.subplots(3, 1, sharex=True, figsize=figsize, gridspec_kw={'height_ratios': [3, 1, 1]}) + if multi_column: + _plt.figtext( + 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) + 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) - fig.suptitle(title, fontsize=14, y=.995, + fig.suptitle(title, fontsize=14, y=.97, fontname=fontname, fontweight='bold', color='black') fig.set_facecolor('white') if subtitle: if isinstance(returns, _pd.Series): - axes[0].set_title("\n%s - %s ; Sharpe: %.2f " % ( + 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) @@ -118,10 +132,10 @@ def snapshot(returns, grayscale=False, figsize=(10, 8), 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) + # axes[0].legend(fontsize=12) dd = _stats.to_drawdown_series(returns) * 100 - ddmin = min(_utils._round_to_closest(abs(dd.min()), 5)) + ddmin = _utils._round_to_closest(abs(dd.min()), 5) ddmin_ticks = 5 if ddmin > 50: ddmin_ticks = ddmin / 4 @@ -141,13 +155,13 @@ def snapshot(returns, grayscale=False, figsize=(10, 8), 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[1], alpha=.25) + axes[1].fill_between(dd.index, 0, dd, color=colors[2], alpha=.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].set_yscale("symlog" if log_scale else "linear") - axes[1].legend(fontsize=12) + # axes[1].legend(fontsize=12) axes[2].set_ylabel('Daily Return', fontname=fontname, fontweight='bold', fontsize=12) @@ -160,17 +174,17 @@ def snapshot(returns, grayscale=False, figsize=(10, 8), 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) + # axes[2].legend(fontsize=12) - retmax = max(_utils._round_to_closest(returns.max() * 100, 5)) - retmin = min(_utils._round_to_closest(returns.min() * 100, 5)) + retmax = _utils._round_to_closest(returns.max() * 100, 5) + retmin = _utils._round_to_closest(returns.min() * 100, 5) retdiff = (retmax - retmin) steps = 5 if retdiff > 50: steps = retdiff / 5 elif retdiff > 30: steps = retdiff / 4 - steps = int(_utils._round_to_closest(steps, 5)) + steps = _utils._round_to_closest(steps, 5) axes[2].set_yticks(_np.arange(retmin, retmax, step=steps)) for ax in axes: @@ -463,7 +477,7 @@ def distribution(returns, fontname='Arial', grayscale=False, ylabel=True, return fig -def histogram(returns, benchmark, resample='M', fontname='Arial', +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): @@ -673,7 +687,7 @@ def monthly_heatmap(returns, benchmark, annot_size=10, figsize=(10, 5), returns = _stats.monthly_returns(returns, eoy=eoy, compounded=compounded) * 100 - fig_height = len(returns) / 3 + fig_height = len(returns) / 2.5 if figsize is None: size = list(_plt.gcf().get_size_inches()) @@ -682,7 +696,7 @@ 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.04, 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) diff --git a/quantstats/reports.py b/quantstats/reports.py index 58b624d9..a8fd0462 100644 --- a/quantstats/reports.py +++ b/quantstats/reports.py @@ -26,7 +26,6 @@ ) from base64 import b64encode as _b64encode import re as _regex - from tabulate import tabulate as _tabulate from . import ( __version__, stats as _stats, @@ -34,7 +33,6 @@ ) from dateutil.relativedelta import relativedelta from io import StringIO - try: from IPython.display import ( display as iDisplay, HTML as iHTML @@ -51,10 +49,12 @@ def _get_trading_periods(periods_per_year=252): def _match_dates(returns, benchmark): - returns = returns.loc[ - max(returns.ne(0).idxmax(), benchmark.ne(0).idxmax()):] - benchmark = benchmark.loc[ - max(returns.ne(0).idxmax(), benchmark.ne(0).idxmax()):] + if isinstance(returns, _pd.DataFrame): + loc = max(returns[returns.columns[0]].ne(0).idxmax(), benchmark.ne(0).idxmax()) + else: + loc = max(returns.ne(0).idxmax(), benchmark.ne(0).idxmax()) + returns = returns.loc[loc:] + benchmark = benchmark.loc[loc:] return returns, benchmark @@ -62,11 +62,14 @@ def _match_dates(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=False, **kwargs): + figfmt='svg', template_path=None, match_dates=True, **kwargs): if output is None and not _utils._in_notebook(): raise ValueError("`output` must be specified") + if match_dates: + returns = returns.dropna() + win_year, win_half_year = _get_trading_periods(periods_per_year) tpl = "" @@ -75,9 +78,15 @@ def html(returns, benchmark=None, rf=0., grayscale=False, f.close() # prepare timeseries + if match_dates: + returns = returns.dropna() returns = _utils._prepare_returns(returns) strategy_title = kwargs.get('strategy_title', 'Strategy') + if isinstance(returns, _pd.DataFrame): + if len(returns.columns) > 1 and isinstance(strategy_title, str): + strategy_title = list(returns.columns) + if benchmark is not None: benchmark_title = kwargs.get('benchmark_title', 'Benchmark') if kwargs.get('benchmark_title') is None: @@ -220,7 +229,7 @@ def html(returns, benchmark=None, rf=0., grayscale=False, figfile = _utils._file_stream() _plots.histogram(returns, benchmark, grayscale=grayscale, - figsize=(8, 4), subtitle=False, + figsize=(7, 4), subtitle=False, savefig={'fname': figfile, 'format': figfmt}, show=False, ylabel=False, compounded=compounded, prepare_returns=False) @@ -345,9 +354,11 @@ def html(returns, benchmark=None, rf=0., grayscale=False, def full(returns, benchmark=None, rf=0., grayscale=False, figsize=(8, 5), display=True, compounded=True, - periods_per_year=252, match_dates=False, **kwargs): + periods_per_year=252, match_dates=True, **kwargs): # prepare timeseries + if match_dates: + returns = returns.dropna() returns = _utils._prepare_returns(returns) if benchmark is not None: benchmark = _utils._prepare_benchmark(benchmark, returns.index, rf) @@ -360,6 +371,10 @@ def full(returns, benchmark=None, rf=0., grayscale=False, 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 benchmark is not None: benchmark.name = benchmark_title if isinstance(returns, _pd.Series): @@ -396,19 +411,19 @@ def full(returns, benchmark=None, rf=0., grayscale=False, prepare_returns=False, benchmark_title=benchmark_title, strategy_title=strategy_title)) - iDisplay(iHTML('

5 Worst Drawdowns

')) if isinstance(dd, _pd.Series): + iDisplay(iHTML('

Worst 5 Drawdowns

')) if dd_info.empty: iDisplay(iHTML("

(no drawdowns)

")) else: iDisplay(dd_info) elif isinstance(dd, _pd.DataFrame): for ptf, dd_info in dd_info_dict.items(): + iDisplay(iHTML('

%s - Worst 5 Drawdowns

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

(no drawdowns)

")) else: - iDisplay(iHTML("

%s

" % ptf)) iDisplay(dd_info) iDisplay(iHTML('

Strategy Visualization

')) @@ -422,7 +437,7 @@ def full(returns, benchmark=None, rf=0., grayscale=False, benchmark_title=benchmark_title, strategy_title=strategy_title) print('\n\n') - print('[5 Worst Drawdowns]\n') + print('[Worst 5 Drawdowns]\n') if isinstance(dd, _pd.Series): if dd_info.empty: print("(no drawdowns)") @@ -449,9 +464,11 @@ def full(returns, benchmark=None, rf=0., grayscale=False, def basic(returns, benchmark=None, rf=0., grayscale=False, figsize=(8, 5), display=True, compounded=True, - periods_per_year=252, match_dates=False, **kwargs): + periods_per_year=252, match_dates=True, **kwargs): # prepare timeseries + if match_dates: + returns = returns.dropna() returns = _utils._prepare_returns(returns) if benchmark is not None: benchmark = _utils._prepare_benchmark(benchmark, returns.index, rf) @@ -464,6 +481,10 @@ def basic(returns, benchmark=None, rf=0., grayscale=False, 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, @@ -495,24 +516,28 @@ def basic(returns, benchmark=None, rf=0., grayscale=False, def metrics(returns, benchmark=None, rf=0., display=True, mode='basic', sep=False, compounded=True, periods_per_year=252, prepare_returns=True, - match_dates=False, **kwargs): + match_dates=True, **kwargs): + if match_dates: + returns = returns.dropna() returns.index = returns.index.tz_localize(None) win_year, _ = _get_trading_periods(periods_per_year) + benchmark_colname = kwargs.get("benchmark_title", "Benchmark") + strategy_colname = kwargs.get("strategy_title", "Strategy") + if benchmark is not None: if isinstance(benchmark, str): - benchmark_col = 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") - benchmark_colname = kwargs.get("benchmark_title", "Benchmark") - strategy_colname = kwargs.get("strategy_title", "Strategy") - if isinstance(returns, _pd.DataFrame): if len(returns.columns) > 1: blank = [''] * len(returns.columns) + if isinstance(strategy_colname, str): + strategy_colname = list(returns.columns) else: blank = [''] @@ -522,7 +547,7 @@ def metrics(returns, benchmark=None, rf=0., display=True, # returns = returns[returns.columns[0]] if prepare_returns: - returns = _utils._prepare_returns(returns) + df = _utils._prepare_returns(returns) if isinstance(returns, _pd.Series): df = _pd.DataFrame({"returns": returns}) @@ -543,27 +568,14 @@ def metrics(returns, benchmark=None, rf=0., display=True, for i, strategy_col in enumerate(returns.columns): df["returns_" + str(i+1)] = returns[strategy_col] - df = df.fillna(0) - - # pct multiplier - pct = 100 if display or "internal" in kwargs else 1 - if kwargs.get("as_pct", False): - pct = 100 - - # return df - dd = _calc_dd(df, display=(display or "internal" in kwargs), - as_pct=kwargs.get("as_pct", False)) - - metrics = _pd.DataFrame() - 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} elif isinstance(returns, _pd.DataFrame): df_strategy_columns = [col for col in df.columns if col != "benchmark"] - s_start = {strategy_col: df[strategy_col].index.strftime('%Y-%m-%d')[0] for strategy_col in df_strategy_columns} - s_end = {strategy_col: df[strategy_col].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: @@ -571,6 +583,18 @@ def metrics(returns, benchmark=None, rf=0., display=True, s_end['benchmark'] = df['benchmark'].index.strftime('%Y-%m-%d')[-1] s_rf['benchmark'] = rf + df = df.fillna(0) + + # pct multiplier + pct = 100 if display or "internal" in kwargs else 1 + if kwargs.get("as_pct", False): + pct = 100 + + # return df + 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 @@ -838,14 +862,23 @@ def metrics(returns, benchmark=None, rf=0., display=True, def plots(returns, benchmark=None, grayscale=False, figsize=(8, 5), mode='basic', compounded=True, - periods_per_year=252, prepare_returns=True, match_dates=False, **kwargs): + 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') + if isinstance(returns, _pd.DataFrame): + if len(returns.columns) > 1: + if isinstance(strategy_colname, str): + strategy_colname = list(returns.columns) + win_year, win_half_year = _get_trading_periods(periods_per_year) + if match_dates is True: + returns = returns.dropna() + if prepare_returns: returns = _utils._prepare_returns(returns) @@ -854,6 +887,8 @@ def plots(returns, benchmark=None, grayscale=False, elif isinstance(returns, _pd.DataFrame): returns.columns = strategy_colname + returns = _pd.DataFrame(returns) + if mode.lower() != 'full': _plots.snapshot(returns, grayscale=grayscale, figsize=(figsize[0], figsize[0]), @@ -867,7 +902,7 @@ def plots(returns, benchmark=None, grayscale=False, compounded=compounded, active=active) elif isinstance(returns, _pd.DataFrame): for col in returns.columns: - _plots.monthly_heatmap(returns[col], benchmark, grayscale=grayscale, + _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) @@ -909,29 +944,32 @@ def plots(returns, benchmark=None, grayscale=False, 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.daily_returns(returns, benchmark, grayscale=grayscale, - figsize=(figsize[0], figsize[0]*.3), + figsize=small_fig_size, show=True, ylabel=False, prepare_returns=False, active=active) if benchmark is not None: _plots.rolling_beta(returns, benchmark, grayscale=grayscale, window1=win_half_year, window2=win_year, - figsize=(figsize[0], figsize[0]*.3), + figsize=small_fig_size, show=True, ylabel=False, prepare_returns=False) - _plots.rolling_volatility( - returns, benchmark, grayscale=grayscale, - figsize=(figsize[0], figsize[0]*.3), show=True, ylabel=False, - period=win_half_year) + _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=(figsize[0], figsize[0]*.3), + figsize=small_fig_size, show=True, ylabel=False, period=win_half_year) _plots.rolling_sortino(returns, grayscale=grayscale, - figsize=(figsize[0], figsize[0]*.3), + figsize=small_fig_size, show=True, ylabel=False, period=win_half_year) if isinstance(returns, _pd.Series): diff --git a/quantstats/stats.py b/quantstats/stats.py index 8f746b9b..1f8d9641 100644 --- a/quantstats/stats.py +++ b/quantstats/stats.py @@ -60,7 +60,7 @@ def get_outliers(data): } if isinstance(returns, _pd.DataFrame): - warn("Pandas DataFrame was passed (Series expeted). " + warn("Pandas DataFrame was passed (Series expected). " "Only first column will be used.") returns = returns.copy() returns.columns = map(str.lower, returns.columns)