quantstats_lumi.reports

   1#!/usr/bin/env python
   2# -*- coding: UTF-8 -*-
   3#
   4# QuantStats: Portfolio analytics for quants
   5# https://github.com/ranaroussi/quantstats
   6#
   7# Copyright 2019-2023 Ran Aroussi
   8#
   9# Licensed under the Apache License, Version 2.0 (the "License");
  10# you may not use this file except in compliance with the License.
  11# You may obtain a copy of the License at
  12#
  13#     http://www.apache.org/licenses/LICENSE-2.0
  14#
  15# Unless required by applicable law or agreed to in writing, software
  16# distributed under the License is distributed on an "AS IS" BASIS,
  17# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  18# See the License for the specific language governing permissions and
  19# limitations under the License.
  20
  21import re as _regex
  22from base64 import b64encode as _b64encode
  23from datetime import datetime as _dt
  24from io import StringIO
  25from math import ceil as _ceil
  26from math import sqrt as _sqrt
  27
  28import numpy as _np
  29import pandas as _pd
  30from dateutil.relativedelta import relativedelta
  31from tabulate import tabulate as _tabulate
  32
  33from . import __version__
  34from . import plots as _plots
  35from . import stats as _stats
  36from . import utils as _utils
  37
  38try:
  39    from IPython.display import HTML as iHTML
  40    from IPython.display import display as iDisplay
  41except ImportError:
  42    from IPython.core.display import HTML as iHTML
  43    from IPython.core.display import display as iDisplay
  44
  45
  46def _get_trading_periods(periods_per_year=365):
  47    """returns trading periods per year and half year"""
  48    half_year = _ceil(periods_per_year / 2)
  49    return periods_per_year, half_year
  50
  51
  52def _match_dates(returns, benchmark):
  53    """match dates of returns and benchmark"""
  54    fix_instance = lambda x: x[x.columns[0]] if isinstance(x, _pd.DataFrame) else x
  55    loc = max(fix_instance(returns).ne(0).idxmax(), fix_instance(benchmark).ne(0).idxmax())
  56    returns = returns.loc[loc:]
  57    benchmark = benchmark.loc[loc:]
  58
  59    return returns, benchmark
  60
  61
  62def html(
  63    returns,
  64    benchmark: _pd.Series = None,
  65    rf: float = 0.0,
  66    grayscale: bool = False,
  67    title: str = "Strategy Tearsheet",
  68    output: str = None,
  69    compounded: bool = True,
  70    periods_per_year: int = 365,
  71    download_filename: str = "tearsheet.html",
  72    figfmt: str = "svg",
  73    template_path: str = None,
  74    match_dates: bool = True,
  75    parameters: dict = None,
  76    log_scale: bool = False,
  77    show_match_volatility: bool = True,
  78    **kwargs,
  79):
  80    """
  81    Generates a full HTML tearsheet with performance metrics and plots
  82
  83    Parameters
  84    ----------
  85    returns : pd.Series, pd.DataFrame
  86        Strategy returns
  87    benchmark : pd.Series, optional
  88        Benchmark returns
  89    rf : float, optional
  90        Risk-free rate, default is 0
  91    grayscale : bool, optional
  92        Plot in grayscale, default is False
  93    title : str, optional
  94        Title of the HTML report, default is "Strategy Tearsheet"
  95    output : str, optional
  96        Output file path
  97    compounded : bool, optional
  98        Whether to use compounded returns, default is True
  99    periods_per_year : int, optional
 100        Trading periods per year, default is 365
 101    download_filename : str, optional
 102        Download filename, default is "tearsheet.html"
 103    figfmt : str, optional
 104        Figure format, default is "svg"
 105    template_path : str, optional
 106        Custom template path
 107    match_dates : bool, optional
 108        Match dates of returns and benchmark, default is True
 109    parameters : dict, optional
 110        Strategy parameters
 111
 112    Returns
 113    -------
 114    None
 115    """
 116
 117    if output is None and not _utils._in_notebook():
 118        raise ValueError("`output` must be specified")
 119
 120    if match_dates:
 121        returns = returns.dropna()
 122
 123    win_year, win_half_year = _get_trading_periods(periods_per_year)
 124
 125    tpl = ""
 126    with open(template_path or __file__[:-4] + ".html", encoding='utf-8') as f:
 127        tpl = f.read()
 128        f.close()
 129
 130    # prepare timeseries
 131    if match_dates:
 132        returns = returns.dropna()
 133    returns = _utils._prepare_returns(returns)
 134
 135    strategy_title = kwargs.get("strategy_title", "Strategy")
 136    if (
 137        isinstance(returns, _pd.DataFrame)
 138        and len(returns.columns) > 1
 139        and isinstance(strategy_title, str)
 140    ):
 141        strategy_title = list(returns.columns)
 142
 143    if benchmark is not None:
 144        benchmark_title = kwargs.get("benchmark_title", "Benchmark")
 145        if kwargs.get("benchmark_title") is None:
 146            if isinstance(benchmark, str):
 147                benchmark_title = benchmark
 148            elif isinstance(benchmark, _pd.Series):
 149                benchmark_title = benchmark.name
 150            elif isinstance(benchmark, _pd.DataFrame):
 151                benchmark_title = benchmark[benchmark.columns[0]].name
 152
 153        tpl = tpl.replace(
 154            "{{benchmark_title}}", f"Benchmark is {benchmark_title.upper()} | "
 155        )
 156        benchmark = _utils._prepare_benchmark(benchmark, returns.index, rf)
 157        if match_dates is True:
 158            returns, benchmark = _match_dates(returns, benchmark)
 159    else:
 160        benchmark_title = None
 161
 162    # Assign names/columns after all preparations and matching
 163    if benchmark is not None:
 164        benchmark.name = benchmark_title
 165    if isinstance(returns, _pd.Series):
 166        returns.name = strategy_title
 167    elif isinstance(returns, _pd.DataFrame):
 168        returns.columns = strategy_title
 169
 170    # Check for no trades condition
 171    no_trades_occurred = False
 172    if returns.empty:
 173        no_trades_occurred = True
 174    else:
 175        sum_abs_returns = 0.0
 176        if isinstance(returns, _pd.Series):
 177            # Assuming returns is numeric after _prepare_returns
 178            sum_abs_returns = returns.abs().sum()
 179        elif isinstance(returns, _pd.DataFrame):
 180            # Assuming returns DataFrame columns are numeric after _prepare_returns
 181            sum_abs_returns = returns.select_dtypes(include=[_np.number]).abs().sum().sum()
 182        
 183        if abs(sum_abs_returns) < 1e-9: # Using a small epsilon for float comparison
 184            no_trades_occurred = True
 185
 186    no_trades_html_message = ""
 187    if no_trades_occurred:
 188        no_trades_html_message = "<div style='text-align: center; padding: 10px; background-color: #fff3cd; color: #856404; border: 1px solid #ffeeba; border-radius: .25rem; margin-bottom: 1rem;'><strong>Note:</strong> No trades or significant activity occurred during this period. Metrics shown below may reflect this.</div>"
 189
 190    date_range = returns.index.strftime("%e %b, %Y")
 191    tpl = tpl.replace("{{date_range}}", date_range[0] + " - " + date_range[-1])
 192    tpl = tpl.replace("{{title}}", title)
 193    tpl = tpl.replace("{{v}}", __version__)
 194
 195    if benchmark is not None:
 196        benchmark.name = benchmark_title
 197    if isinstance(returns, _pd.Series):
 198        returns.name = strategy_title
 199    elif isinstance(returns, _pd.DataFrame):
 200        returns.columns = strategy_title
 201
 202    mtrx = metrics(
 203        returns=returns,
 204        benchmark=benchmark,
 205        rf=rf,
 206        display=False,
 207        mode="full",
 208        sep=True,
 209        internal="True",
 210        compounded=compounded,
 211        periods_per_year=periods_per_year,
 212        prepare_returns=False,
 213        benchmark_title=benchmark_title,
 214        strategy_title=strategy_title,
 215    )[2:]
 216
 217    mtrx.index.name = "Metric"
 218    # tpl = tpl.replace("{{metrics}}", _html_table(mtrx)) # Original line
 219    
 220    # Modified replacement for metrics table
 221    metrics_table_html = _html_table(mtrx)
 222    # The "no trades" message is no longer prepended here.
 223    tpl = tpl.replace("{{metrics}}", metrics_table_html, 1)
 224
 225    # Add all of the summary metrics
 226    # Ensure these are cast to str and the no_trades_html_message is NOT with CAGR here.
 227
 228    # CAGR #
 229    cagr_value = mtrx.loc["CAGR% (Annual Return)", strategy_title]
 230    # Ensure CAGR is formatted as a percentage string with 2 decimals
 231    if isinstance(cagr_value, float):
 232        cagr_str = f"{cagr_value:.2f}%"
 233    else:
 234        cagr_str = str(cagr_value)
 235    tpl = tpl.replace("{{cagr}}", cagr_str)
 236
 237    # Total Return #
 238    total_return_value = mtrx.loc["Total Return", strategy_title]
 239    # Ensure Total Return is formatted as a percentage string with 2 decimals
 240    if isinstance(total_return_value, float):
 241        total_return_str = f"{total_return_value:.2f}%"
 242    else:
 243        total_return_str = str(total_return_value)
 244    tpl = tpl.replace("{{total_return}}", total_return_str)
 245
 246    # Max Drawdown #
 247    max_drawdown_value = mtrx.loc["Max Drawdown", strategy_title]
 248    if isinstance(max_drawdown_value, float):
 249        max_drawdown_str = f"{max_drawdown_value:.2f}%"
 250    else:
 251        max_drawdown_str = str(max_drawdown_value)
 252    tpl = tpl.replace("{{max_drawdown}}", max_drawdown_str)
 253
 254    # RoMaD #
 255    romad_value = mtrx.loc["RoMaD", strategy_title]
 256    if isinstance(romad_value, float):
 257        romad_str = f"{romad_value:.2f}"
 258    else:
 259        romad_str = str(romad_value)
 260    tpl = tpl.replace("{{romad}}", romad_str)
 261
 262    # Longest Drawdown Duration #
 263    longest_dd_days_value = mtrx.loc["Longest DD Days", strategy_title]
 264    tpl = tpl.replace("{{longest_dd_days}}", str(longest_dd_days_value))
 265
 266    # Sharpe #
 267    sharpe_value = mtrx.loc["Sharpe", strategy_title]
 268    if isinstance(sharpe_value, float):
 269        sharpe_str = f"{sharpe_value:.2f}"
 270    else:
 271        sharpe_str = str(sharpe_value)
 272    tpl = tpl.replace("{{sharpe}}", sharpe_str)
 273
 274    # Sortino #
 275    sortino_value = mtrx.loc["Sortino", strategy_title]
 276    if isinstance(sortino_value, float):
 277        sortino_str = f"{sortino_value:.2f}"
 278    else:
 279        sortino_str = str(sortino_value)
 280    tpl = tpl.replace("{{sortino}}", sortino_str)
 281
 282
 283    if isinstance(returns, _pd.DataFrame):
 284        num_cols = len(returns.columns)
 285        for i in reversed(range(num_cols + 1, num_cols + 3)):
 286            str_td = "<td></td>" * i
 287            tpl = tpl.replace(
 288                f"<tr>{str_td}</tr>", '<tr><td colspan="{}"><hr></td></tr>'.format(i)
 289            )
 290
 291    tpl = tpl.replace(
 292        "<tr><td></td><td></td><td></td></tr>", '<tr><td colspan="3"><hr></td></tr>'
 293    )
 294    tpl = tpl.replace(
 295        "<tr><td></td><td></td></tr>", '<tr><td colspan="2"><hr></td></tr>'
 296    )
 297
 298    if parameters is not None:
 299        tpl = tpl.replace("{{parameters_section}}", parameters_section(parameters))
 300
 301    if benchmark is not None:
 302        yoy = _stats.compare(
 303            returns, benchmark, "YE", compounded=compounded, prepare_returns=False
 304        )
 305        if isinstance(returns, _pd.Series):
 306            yoy.columns = [benchmark_title, strategy_title, "Multiplier", "Won"]
 307        elif isinstance(returns, _pd.DataFrame):
 308            yoy.columns = list(
 309                _pd.core.common.flatten([benchmark_title, strategy_title])
 310            )
 311        yoy.index.name = "Year"
 312        tpl = tpl.replace("{{eoy_title}}", "<h3>EOY Returns vs Benchmark</h3>")
 313        tpl = tpl.replace("{{eoy_table}}", _html_table(yoy))
 314    else:
 315        # pct multiplier
 316        yoy = _pd.DataFrame(_utils.group_returns(returns, returns.index.year) * 100)
 317        if isinstance(returns, _pd.Series):
 318            yoy.columns = ["Return"]
 319            yoy["Cumulative"] = _utils.group_returns(returns, returns.index.year, True)
 320            yoy["Return"] = yoy["Return"].round(2).astype(str) + "%"
 321            yoy["Cumulative"] = (yoy["Cumulative"] * 100).round(2).astype(str) + "%"
 322        elif isinstance(returns, _pd.DataFrame):
 323            # Don't show cumulative for multiple strategy portfolios
 324            # just show compounded like when we have a benchmark
 325            yoy.columns = list(_pd.core.common.flatten(strategy_title))
 326
 327        yoy.index.name = "Year"
 328        tpl = tpl.replace("{{eoy_title}}", "<h3>EOY Returns</h3>")
 329        tpl = tpl.replace("{{eoy_table}}", _html_table(yoy))
 330
 331    if isinstance(returns, _pd.Series):
 332        dd = _stats.to_drawdown_series(returns)
 333        dd_info = _stats.drawdown_details(dd).sort_values(
 334            by="max drawdown", ascending=True
 335        )[:10]
 336        dd_info = dd_info[["start", "end", "max drawdown", "days"]]
 337        dd_info.columns = ["Started", "Recovered", "Drawdown", "Days"]
 338        tpl = tpl.replace("{{dd_info}}", _html_table(dd_info, False))
 339    elif isinstance(returns, _pd.DataFrame):
 340        dd_info_list = []
 341        for col in returns.columns:
 342            dd = _stats.to_drawdown_series(returns[col])
 343            dd_info = _stats.drawdown_details(dd).sort_values(
 344                by="max drawdown", ascending=True
 345            )[:10]
 346            dd_info = dd_info[["start", "end", "max drawdown", "days"]]
 347            dd_info.columns = ["Started", "Recovered", "Drawdown", "Days"]
 348            dd_info_list.append(_html_table(dd_info, False))
 349
 350        dd_html_table = ""
 351        for html_str, col in zip(dd_info_list, returns.columns):
 352            dd_html_table = (
 353                dd_html_table + f"<h3>{col}</h3><br>" + StringIO(html_str).read()
 354            )
 355        tpl = tpl.replace("{{dd_info}}", dd_html_table)
 356
 357    active = kwargs.get("active_returns", False)
 358    # plots
 359    plot_returns = _plots.log_returns if log_scale else _plots.returns
 360    placeholder_returns = "{{log_returns}}" if log_scale else "{{returns}}"
 361
 362    figfile = _utils._file_stream()
 363    plot_returns(
 364        returns,
 365        benchmark,
 366        grayscale=grayscale,
 367        figsize=(8, 5),
 368        subtitle=False,
 369        savefig={"fname": figfile, "format": figfmt},
 370        show=False,
 371        ylabel=False,
 372        cumulative=compounded,
 373        prepare_returns=False,
 374    )
 375    first_plot_html = _embed_figure(figfile, figfmt) # Get the HTML for the first plot
 376
 377    # Prepend the no_trades_html_message if no trades occurred, then add the plot
 378    if no_trades_occurred:
 379        tpl = tpl.replace(placeholder_returns, no_trades_html_message + first_plot_html, 1)
 380    else:
 381        tpl = tpl.replace(placeholder_returns, first_plot_html, 1)
 382
 383    if benchmark is not None and show_match_volatility:
 384        figfile = _utils._file_stream()
 385        plot_returns(
 386            returns,
 387            benchmark,
 388            match_volatility=True,
 389            grayscale=grayscale,
 390            figsize=(8, 5),
 391            subtitle=False,
 392            savefig={"fname": figfile, "format": figfmt},
 393            show=False,
 394            ylabel=False,
 395            cumulative=compounded,
 396            prepare_returns=False,
 397        )
 398        tpl = tpl.replace("{{vol_returns}}", _embed_figure(figfile, figfmt))
 399
 400    figfile = _utils._file_stream()
 401    _plots.yearly_returns(
 402        returns,
 403        benchmark,
 404        grayscale=grayscale,
 405        figsize=(8, 4),
 406        subtitle=False,
 407        savefig={"fname": figfile, "format": figfmt},
 408        show=False,
 409        ylabel=False,
 410        compounded=compounded,
 411        prepare_returns=False,
 412    )
 413    tpl = tpl.replace("{{eoy_returns}}", _embed_figure(figfile, figfmt))
 414
 415    figfile = _utils._file_stream()
 416    _plots.histogram(
 417        returns,
 418        benchmark,
 419        grayscale=grayscale,
 420        figsize=(7, 4),
 421        subtitle=False,
 422        savefig={"fname": figfile, "format": figfmt},
 423        show=False,
 424        ylabel=False,
 425        compounded=compounded,
 426        prepare_returns=False,
 427    )
 428    tpl = tpl.replace("{{monthly_dist}}", _embed_figure(figfile, figfmt))
 429
 430    figfile = _utils._file_stream()
 431    _plots.daily_returns(
 432        returns,
 433        benchmark,
 434        grayscale=grayscale,
 435        figsize=(8, 3),
 436        subtitle=False,
 437        savefig={"fname": figfile, "format": figfmt},
 438        show=False,
 439        ylabel=False,
 440        prepare_returns=False,
 441        active=active,
 442    )
 443    tpl = tpl.replace("{{daily_returns}}", _embed_figure(figfile, figfmt))
 444
 445    if benchmark is not None:
 446        figfile = _utils._file_stream()
 447        _plots.rolling_beta(
 448            returns,
 449            benchmark,
 450            grayscale=grayscale,
 451            figsize=(8, 3),
 452            subtitle=False,
 453            window1=win_half_year,
 454            window2=win_year,
 455            savefig={"fname": figfile, "format": figfmt},
 456            show=False,
 457            ylabel=False,
 458            prepare_returns=False,
 459        )
 460        tpl = tpl.replace("{{rolling_beta}}", _embed_figure(figfile, figfmt))
 461
 462    figfile = _utils._file_stream()
 463    _plots.rolling_volatility(
 464        returns,
 465        benchmark,
 466        grayscale=grayscale,
 467        figsize=(8, 3),
 468        subtitle=False,
 469        savefig={"fname": figfile, "format": figfmt},
 470        show=False,
 471        ylabel=False,
 472        period=win_half_year,
 473        periods_per_year=win_year,
 474    )
 475    tpl = tpl.replace("{{rolling_vol}}", _embed_figure(figfile, figfmt))
 476
 477    figfile = _utils._file_stream()
 478    _plots.rolling_sharpe(
 479        returns,
 480        grayscale=grayscale,
 481        figsize=(8, 3),
 482        subtitle=False,
 483        savefig={"fname": figfile, "format": figfmt},
 484        show=False,
 485        ylabel=False,
 486        period=win_half_year,
 487        periods_per_year=win_year,
 488    )
 489    tpl = tpl.replace("{{rolling_sharpe}}", _embed_figure(figfile, figfmt))
 490
 491    figfile = _utils._file_stream()
 492    _plots.rolling_sortino(
 493        returns,
 494        grayscale=grayscale,
 495        figsize=(8, 3),
 496        subtitle=False,
 497        savefig={"fname": figfile, "format": figfmt},
 498        show=False,
 499        ylabel=False,
 500        period=win_half_year,
 501        periods_per_year=win_year,
 502    )
 503    tpl = tpl.replace("{{rolling_sortino}}", _embed_figure(figfile, figfmt))
 504
 505    figfile = _utils._file_stream()
 506    if isinstance(returns, _pd.Series):
 507        _plots.drawdowns_periods(
 508            returns,
 509            grayscale=grayscale,
 510            figsize=(8, 4),
 511            subtitle=False,
 512            title=returns.name,
 513            savefig={"fname": figfile, "format": figfmt},
 514            show=False,
 515            ylabel=False,
 516            compounded=compounded,
 517            prepare_returns=False,
 518            log_scale=log_scale,
 519        )
 520        tpl = tpl.replace("{{dd_periods}}", _embed_figure(figfile, figfmt))
 521    elif isinstance(returns, _pd.DataFrame):
 522        embed = []
 523        for col in returns.columns:
 524            _plots.drawdowns_periods(
 525                returns[col],
 526                grayscale=grayscale,
 527                figsize=(8, 4),
 528                subtitle=False,
 529                title=col,
 530                savefig={"fname": figfile, "format": figfmt},
 531                show=False,
 532                ylabel=False,
 533                compounded=compounded,
 534                prepare_returns=False,
 535            )
 536            embed.append(figfile)
 537        tpl = tpl.replace("{{dd_periods}}", _embed_figure(embed, figfmt))
 538
 539    figfile = _utils._file_stream()
 540    _plots.drawdown(
 541        returns,
 542        grayscale=grayscale,
 543        figsize=(8, 3),
 544        subtitle=False,
 545        savefig={"fname": figfile, "format": figfmt},
 546        show=False,
 547        ylabel=False,
 548    )
 549    tpl = tpl.replace("{{dd_plot}}", _embed_figure(figfile, figfmt))
 550
 551    figfile = _utils._file_stream()
 552    if isinstance(returns, _pd.Series):
 553        _plots.monthly_heatmap(
 554            returns,
 555            benchmark,
 556            grayscale=grayscale,
 557            figsize=(8, 4),
 558            cbar=False,
 559            returns_label=returns.name,
 560            savefig={"fname": figfile, "format": figfmt},
 561            show=False,
 562            ylabel=False,
 563            compounded=compounded,
 564            active=active,
 565        )
 566        tpl = tpl.replace("{{monthly_heatmap}}", _embed_figure(figfile, figfmt))
 567    elif isinstance(returns, _pd.DataFrame):
 568        embed = []
 569        for col in returns.columns:
 570            _plots.monthly_heatmap(
 571                returns[col],
 572                benchmark,
 573                grayscale=grayscale,
 574                figsize=(8, 4),
 575                cbar=False,
 576                returns_label=col,
 577                savefig={"fname": figfile, "format": figfmt},
 578                show=False,
 579                ylabel=False,
 580                compounded=compounded,
 581                active=active,
 582            )
 583            embed.append(figfile)
 584        tpl = tpl.replace("{{monthly_heatmap}}", _embed_figure(embed, figfmt))
 585
 586    figfile = _utils._file_stream()
 587
 588    if isinstance(returns, _pd.Series):
 589        _plots.distribution(
 590            returns,
 591            grayscale=grayscale,
 592            figsize=(8, 4),
 593            subtitle=False,
 594            title=returns.name,
 595            savefig={"fname": figfile, "format": figfmt},
 596            show=False,
 597            ylabel=False,
 598            compounded=compounded,
 599            prepare_returns=False,
 600        )
 601        tpl = tpl.replace("{{returns_dist}}", _embed_figure(figfile, figfmt))
 602    elif isinstance(returns, _pd.DataFrame):
 603        embed = []
 604        for col in returns.columns:
 605            _plots.distribution(
 606                returns[col],
 607                grayscale=grayscale,
 608                figsize=(8, 4),
 609                subtitle=False,
 610                title=col,
 611                savefig={"fname": figfile, "format": figfmt},
 612                show=False,
 613                ylabel=False,
 614                compounded=compounded,
 615                prepare_returns=False,
 616            )
 617            embed.append(figfile)
 618        tpl = tpl.replace("{{returns_dist}}", _embed_figure(embed, figfmt))
 619
 620    tpl = _regex.sub(r"\{\{(.*?)\}\}", "", tpl)
 621    tpl = tpl.replace("white-space:pre;", "")
 622
 623    if output is None:
 624        # _open_html(tpl)
 625        _download_html(tpl, download_filename)
 626        return
 627
 628    with open(output, "w", encoding="utf-8") as f:
 629        f.write(tpl)
 630
 631    print(f"HTML report saved to: {output}")
 632
 633    # Return the metrics
 634    return mtrx
 635
 636
 637def full(
 638    returns,
 639    benchmark=None,
 640    rf=0.0,
 641    grayscale=False,
 642    figsize=(8, 5),
 643    display=True,
 644    compounded=True,
 645    periods_per_year=365,
 646    match_dates=True,
 647    **kwargs,
 648):
 649    """calculates and plots full performance metrics"""
 650
 651    # prepare timeseries
 652    if match_dates:
 653        returns = returns.dropna()
 654    returns = _utils._prepare_returns(returns)
 655    if benchmark is not None:
 656        benchmark = _utils._prepare_benchmark(benchmark, returns.index, rf)
 657        if match_dates is True:
 658            returns, benchmark = _match_dates(returns, benchmark)
 659
 660    benchmark_title = None
 661    if benchmark is not None:
 662        benchmark_title = kwargs.get("benchmark_title", "Benchmark")
 663    strategy_title = kwargs.get("strategy_title", "Strategy")
 664    active = kwargs.get("active_returns", False)
 665
 666    if (
 667        isinstance(returns, _pd.DataFrame)
 668        and len(returns.columns) > 1
 669        and isinstance(strategy_title, str)
 670    ):
 671        strategy_title = list(returns.columns)
 672
 673    if benchmark is not None:
 674        benchmark.name = benchmark_title
 675    if isinstance(returns, _pd.Series):
 676        returns.name = strategy_title
 677    elif isinstance(returns, _pd.DataFrame):
 678        returns.columns = strategy_title
 679
 680    dd = _stats.to_drawdown_series(returns)
 681
 682    if isinstance(dd, _pd.Series):
 683        col = _stats.drawdown_details(dd).columns[4]
 684        dd_info = _stats.drawdown_details(dd).sort_values(by=col, ascending=True)[:5]
 685        if not dd_info.empty:
 686            dd_info.index = range(1, min(6, len(dd_info) + 1))
 687            dd_info.columns = map(lambda x: str(x).title(), dd_info.columns)
 688    elif isinstance(dd, _pd.DataFrame):
 689        col = _stats.drawdown_details(dd).columns.get_level_values(1)[4]
 690        dd_info_dict = {}
 691        for ptf in dd.columns:
 692            dd_info = _stats.drawdown_details(dd[ptf]).sort_values(
 693                by=col, ascending=True
 694            )[:5]
 695            if not dd_info.empty:
 696                dd_info.index = range(1, min(6, len(dd_info) + 1))
 697                dd_info.columns = map(lambda x: str(x).title(), dd_info.columns)
 698            dd_info_dict[ptf] = dd_info
 699
 700    if _utils._in_notebook():
 701        iDisplay(iHTML("<h4>Performance Metrics</h4>"))
 702        iDisplay(
 703            metrics(
 704                returns=returns,
 705                benchmark=benchmark,
 706                rf=rf,
 707                display=display,
 708                mode="full",
 709                compounded=compounded,
 710                periods_per_year=periods_per_year,
 711                prepare_returns=False,
 712                benchmark_title=benchmark_title,
 713                strategy_title=strategy_title,
 714            )
 715        )
 716
 717        if isinstance(dd, _pd.Series):
 718            iDisplay(iHTML('<h4 style="margin-bottom:20px">Worst 5 Drawdowns</h4>'))
 719            if dd_info.empty:
 720                iDisplay(iHTML("<p>(no drawdowns)</p>"))
 721            else:
 722                iDisplay(dd_info)
 723        elif isinstance(dd, _pd.DataFrame):
 724            for ptf, dd_info in dd_info_dict.items():
 725                iDisplay(
 726                    iHTML(
 727                        '<h4 style="margin-bottom:20px">%s - Worst 5 Drawdowns</h4>'
 728                        % ptf
 729                    )
 730                )
 731                if dd_info.empty:
 732                    iDisplay(iHTML("<p>(no drawdowns)</p>"))
 733                else:
 734                    iDisplay(dd_info)
 735
 736        iDisplay(iHTML("<h4>Strategy Visualization</h4>"))
 737    else:
 738        print("[Performance Metrics]\n")
 739        metrics(
 740            returns=returns,
 741            benchmark=benchmark,
 742            rf=rf,
 743            display=display,
 744            mode="full",
 745            compounded=compounded,
 746            periods_per_year=periods_per_year,
 747            prepare_returns=False,
 748            benchmark_title=benchmark_title,
 749            strategy_title=strategy_title,
 750        )
 751        print("\n\n")
 752        print("[Worst 5 Drawdowns]\n")
 753        if isinstance(dd, _pd.Series):
 754            if dd_info.empty:
 755                print("(no drawdowns)")
 756            else:
 757                print(
 758                    _tabulate(
 759                        dd_info, headers="keys", tablefmt="simple", floatfmt=".2f"
 760                    )
 761                )
 762        elif isinstance(dd, _pd.DataFrame):
 763            for ptf, dd_info in dd_info_dict.items():
 764                if dd_info.empty:
 765                    print("(no drawdowns)")
 766                else:
 767                    print(f"{ptf}\n")
 768                    print(
 769                        _tabulate(
 770                            dd_info, headers="keys", tablefmt="simple", floatfmt=".2f"
 771                        )
 772                    )
 773
 774        print("\n\n")
 775        print("[Strategy Visualization]\nvia Matplotlib")
 776
 777    plots(
 778        returns=returns,
 779        benchmark=benchmark,
 780        grayscale=grayscale,
 781        figsize=figsize,
 782        mode="full",
 783        periods_per_year=periods_per_year,
 784        prepare_returns=False,
 785        benchmark_title=benchmark_title,
 786        strategy_title=strategy_title,
 787        active=active,
 788    )
 789
 790def basic(
 791    returns,
 792    benchmark=None,
 793    rf=0.0,
 794    grayscale=False,
 795    figsize=(8, 5),
 796    display=True,
 797    compounded=True,
 798    periods_per_year=365,
 799    match_dates=True,
 800    **kwargs,
 801):
 802    """calculates and plots basic performance metrics"""
 803
 804    # prepare timeseries
 805    if match_dates:
 806        returns = returns.dropna()
 807    returns = _utils._prepare_returns(returns)
 808    if benchmark is not None:
 809        benchmark = _utils._prepare_benchmark(benchmark, returns.index, rf)
 810        if match_dates is True:
 811            returns, benchmark = _match_dates(returns, benchmark)
 812
 813    benchmark_title = None
 814    if benchmark is not None:
 815        benchmark_title = kwargs.get("benchmark_title", "Benchmark")
 816    strategy_title = kwargs.get("strategy_title", "Strategy")
 817    active = kwargs.get("active_returns", False)
 818
 819    if (
 820        isinstance(returns, _pd.DataFrame)
 821        and len(returns.columns) > 1
 822        and isinstance(strategy_title, str)
 823    ):
 824        strategy_title = list(returns.columns)
 825
 826    if _utils._in_notebook():
 827        iDisplay(iHTML("<h4>Performance Metrics</h4>"))
 828        metrics(
 829            returns=returns,
 830            benchmark=benchmark,
 831            rf=rf,
 832            display=display,
 833            mode="basic",
 834            compounded=compounded,
 835            periods_per_year=periods_per_year,
 836            prepare_returns=False,
 837            benchmark_title=benchmark_title,
 838            strategy_title=strategy_title,
 839        )
 840        iDisplay(iHTML("<h4>Strategy Visualization</h4>"))
 841    else:
 842        print("[Performance Metrics]\n")
 843        metrics(
 844            returns=returns,
 845            benchmark=benchmark,
 846            rf=rf,
 847            display=display,
 848            mode="basic",
 849            compounded=compounded,
 850            periods_per_year=periods_per_year,
 851            prepare_returns=False,
 852            benchmark_title=benchmark_title,
 853            strategy_title=strategy_title,
 854        )
 855
 856        print("\n\n")
 857        print("[Strategy Visualization]\nvia Matplotlib")
 858
 859    plots(
 860        returns=returns,
 861        benchmark=benchmark,
 862        grayscale=grayscale,
 863        figsize=figsize,
 864        mode="basic",
 865        periods_per_year=periods_per_year,
 866        prepare_returns=False,
 867        benchmark_title=benchmark_title,
 868        strategy_title=strategy_title,
 869        active=active,
 870    )
 871
 872def parameters_section(parameters):
 873    """returns a formatted section for strategy parameters"""
 874    if parameters is None:
 875        return ""
 876
 877    tpl = """
 878    <div id="params">
 879        <h3>Parameters Used</h3>
 880        <table style="width:100%; font-size: 12px">
 881    """
 882
 883    # Add titles to the table
 884    tpl += "<thead><tr><th>Parameter</th><th>Value</th></tr></thead>"
 885
 886    for key, value in parameters.items():
 887        # Make sure that the value is something that can be displayed
 888        if not isinstance(value, (int, float, str)):
 889            value = str(value)
 890
 891        tpl += f"<tr><td>{key}</td><td>{value}</td></tr>"
 892    tpl += """
 893        </table>
 894    </div>
 895    """
 896
 897    return tpl
 898
 899def metrics(
 900    returns,
 901    benchmark=None,
 902    rf=0.0,
 903    display=True,
 904    mode="basic",
 905    sep=False,
 906    compounded=True,
 907    periods_per_year=365,
 908    prepare_returns=True,
 909    match_dates=True,
 910    **kwargs,
 911):
 912    """calculates and displays various performance metrics"""
 913
 914    if match_dates:
 915        returns = returns.dropna()
 916    returns.index = returns.index.tz_localize(None)
 917    win_year, _ = _get_trading_periods(periods_per_year)
 918
 919    benchmark_colname = kwargs.get("benchmark_title", "Benchmark")
 920    strategy_colname = kwargs.get("strategy_title", "Strategy")
 921
 922    if benchmark is not None:
 923        if isinstance(benchmark, str):
 924            benchmark_colname = f"Benchmark ({benchmark.upper()})"
 925        elif isinstance(benchmark, _pd.DataFrame) and len(benchmark.columns) > 1:
 926            raise ValueError(
 927                "`benchmark` must be a pandas Series, "
 928                "but a multi-column DataFrame was passed"
 929            )
 930
 931    if isinstance(returns, _pd.DataFrame):
 932        if len(returns.columns) > 1:
 933            blank = [""] * len(returns.columns)
 934            if isinstance(strategy_colname, str):
 935                strategy_colname = list(returns.columns)
 936    else:
 937        blank = [""]
 938
 939    if prepare_returns:
 940        df = _utils._prepare_returns(returns)
 941
 942    if isinstance(returns, _pd.Series):
 943        df = _pd.DataFrame({"returns": returns})
 944    elif isinstance(returns, _pd.DataFrame):
 945        df = _pd.DataFrame(
 946            {
 947                "returns_" + str(i + 1): returns[strategy_col]
 948                for i, strategy_col in enumerate(returns.columns)
 949            }
 950        )
 951
 952    if benchmark is not None:
 953        benchmark = _utils._prepare_benchmark(benchmark, returns.index, rf)
 954        if match_dates is True:
 955            returns, benchmark = _match_dates(returns, benchmark)
 956        df["benchmark"] = benchmark
 957        if isinstance(returns, _pd.Series):
 958            blank = ["", ""]
 959            df["returns"] = returns
 960        elif isinstance(returns, _pd.DataFrame):
 961            blank = [""] * len(returns.columns) + [""]
 962            for i, strategy_col in enumerate(returns.columns):
 963                df["returns_" + str(i + 1)] = returns[strategy_col]
 964
 965    if isinstance(returns, _pd.Series):
 966        s_start = {"returns": df["returns"].index.strftime("%Y-%m-%d")[0]}
 967        s_end = {"returns": df["returns"].index.strftime("%Y-%m-%d")[-1]}
 968        s_rf = {"returns": rf}
 969    elif isinstance(returns, _pd.DataFrame):
 970        df_strategy_columns = [col for col in df.columns if col != "benchmark"]
 971        s_start = {
 972            strategy_col: df[strategy_col].dropna().index.strftime("%Y-%m-%d")[0]
 973            for strategy_col in df_strategy_columns
 974        }
 975        s_end = {
 976            strategy_col: df[strategy_col].dropna().index.strftime("%Y-%m-%d")[-1]
 977            for strategy_col in df_strategy_columns
 978        }
 979        s_rf = {strategy_col: rf for strategy_col in df_strategy_columns}
 980
 981    if "benchmark" in df:
 982        s_start["benchmark"] = df["benchmark"].index.strftime("%Y-%m-%d")[0]
 983        s_end["benchmark"] = df["benchmark"].index.strftime("%Y-%m-%d")[-1]
 984        s_rf["benchmark"] = rf
 985
 986    df = df.fillna(0)
 987
 988    # pct multiplier
 989    pct = 100 if display or "internal" in kwargs else 1
 990    if kwargs.get("as_pct", False):
 991        pct = 100
 992
 993    # return df
 994    dd = _calc_dd(
 995        df,
 996        display=(display or "internal" in kwargs),
 997        as_pct=kwargs.get("as_pct", False),
 998    )
 999
1000    metrics = _pd.DataFrame()
1001    metrics["Start Period"] = _pd.Series(s_start)
1002    metrics["End Period"] = _pd.Series(s_end)
1003    metrics["Risk-Free Rate % "] = _pd.Series(s_rf) * 100
1004    metrics["Time in Market % "] = _stats.exposure(df, prepare_returns=False) * pct
1005
1006    metrics["~"] = blank
1007
1008    if compounded:
1009        metrics["Total Return"] = (_stats.comp(df) * pct).map("{:,.0f}%".format)  # No decimals for readability as it is a large number
1010    else:
1011        metrics["Total Return"] = (df.sum() * pct).map("{:,.2f}%".format)
1012
1013    metrics["CAGR% (Annual Return) "] = _stats.cagr(df, rf, compounded, win_year) * pct
1014
1015    metrics["~~~~~~~~~~~~~~"] = blank
1016
1017    metrics["Sharpe"] = _stats.sharpe(df, rf, win_year, True)
1018    metrics["RoMaD"] = _stats.romad(df, win_year, True)
1019
1020    if benchmark is not None:
1021        metrics["Corr to Benchmark "] = _stats.benchmark_correlation(df, benchmark, True)
1022    metrics["Prob. Sharpe Ratio %"] = (
1023        _stats.probabilistic_sharpe_ratio(df, rf, win_year, False) * pct
1024    )
1025    if mode.lower() == "full":
1026        metrics["Smart Sharpe"] = _stats.smart_sharpe(df, rf, win_year, True)
1027
1028    metrics["Sortino"] = _stats.sortino(df, rf, win_year, True)
1029    if mode.lower() == "full":
1030        metrics["Smart Sortino"] = _stats.smart_sortino(df, rf, win_year, True)
1031
1032    metrics["Sortino/√2"] = metrics["Sortino"] / _sqrt(2)
1033    if mode.lower() == "full":
1034        metrics["Smart Sortino/√2"] = metrics["Smart Sortino"] / _sqrt(2)
1035    metrics["Omega"] = _stats.omega(df, rf, 0.0, win_year)
1036
1037    metrics["~~~~~~~~"] = blank
1038    metrics["Max Drawdown %"] = blank
1039    metrics["Longest DD Days"] = blank
1040
1041    if mode.lower() == "full":
1042        if isinstance(returns, _pd.Series):
1043            ret_vol = (
1044                _stats.volatility(df["returns"], win_year, True, prepare_returns=False)
1045                * pct
1046            )
1047        elif isinstance(returns, _pd.DataFrame):
1048            ret_vol = [
1049                _stats.volatility(
1050                    df[strategy_col], win_year, True, prepare_returns=False
1051                )
1052                * pct
1053                for strategy_col in df_strategy_columns
1054            ]
1055        if "benchmark" in df:
1056            bench_vol = (
1057                _stats.volatility(
1058                    df["benchmark"], win_year, True, prepare_returns=False
1059                )
1060                * pct
1061            )
1062
1063            vol_ = [ret_vol, bench_vol]
1064            if isinstance(ret_vol, list):
1065                metrics["Volatility (ann.) %"] = list(_pd.core.common.flatten(vol_))
1066            else:
1067                metrics["Volatility (ann.) %"] = vol_
1068
1069            if isinstance(returns, _pd.Series):
1070                metrics["R^2"] = _stats.r_squared(
1071                    df["returns"], df["benchmark"], prepare_returns=False
1072                )
1073                metrics["Information Ratio"] = _stats.information_ratio(
1074                    df["returns"], df["benchmark"], prepare_returns=False
1075                )
1076            elif isinstance(returns, _pd.DataFrame):
1077                metrics["R^2"] = (
1078                    [
1079                        _stats.r_squared(
1080                            df[strategy_col], df["benchmark"], prepare_returns=False
1081                        ).round(2)
1082                        for strategy_col in df_strategy_columns
1083                    ]
1084                ) + ["-"]
1085                metrics["Information Ratio"] = (
1086                    [
1087                        _stats.information_ratio(
1088                            df[strategy_col], df["benchmark"], prepare_returns=False
1089                        ).round(2)
1090                        for strategy_col in df_strategy_columns
1091                    ]
1092                ) + ["-"]
1093        else:
1094            if isinstance(returns, _pd.Series):
1095                metrics["Volatility (ann.) %"] = [ret_vol]
1096            elif isinstance(returns, _pd.DataFrame):
1097                metrics["Volatility (ann.) %"] = ret_vol
1098
1099        metrics["Calmar"] = _stats.calmar(df, prepare_returns=False, periods=win_year)
1100        metrics["Skew"] = _stats.skew(df, prepare_returns=False)
1101        metrics["Kurtosis"] = _stats.kurtosis(df, prepare_returns=False)
1102
1103        metrics["~~~~~~~~~~"] = blank
1104
1105        metrics["Expected Daily %%"] = (
1106            _stats.expected_return(df, compounded=compounded, prepare_returns=False)
1107            * pct
1108        )
1109        metrics["Expected Monthly %%"] = (
1110            _stats.expected_return(
1111                df, compounded=compounded, aggregate="ME", prepare_returns=False
1112            )
1113            * pct
1114        )
1115        metrics["Expected Yearly %%"] = (
1116            _stats.expected_return(
1117                df, compounded=compounded, aggregate="YE", prepare_returns=False
1118            )
1119            * pct
1120        )
1121
1122        metrics["Daily Value-at-Risk %"] = -abs(
1123            _stats.var(df, prepare_returns=False) * pct
1124        )
1125        metrics["Expected Shortfall (cVaR) %"] = -abs(
1126            _stats.cvar(df, prepare_returns=False) * pct
1127        )
1128
1129    # returns
1130    metrics["~~"] = blank
1131    comp_func = _stats.comp if compounded else lambda x: _np.sum(x, axis=0)
1132
1133    today = df.index[-1]  # _dt.today()
1134    metrics["MTD %"] = comp_func(df[df.index >= _dt(today.year, today.month, 1)]) * pct
1135
1136    d = today - relativedelta(months=3)
1137    metrics["3M %"] = comp_func(df[df.index >= d]) * pct
1138
1139    d = today - relativedelta(months=6)
1140    metrics["6M %"] = comp_func(df[df.index >= d]) * pct
1141
1142    metrics["YTD %"] = comp_func(df[df.index >= _dt(today.year, 1, 1)]) * pct
1143
1144    d = today - relativedelta(years=1)
1145    metrics["1Y %"] = comp_func(df[df.index >= d]) * pct
1146
1147    d = today - relativedelta(months=35)
1148    metrics["3Y (ann.) %"] = (
1149        _stats.cagr(df[df.index >= d], 0.0, compounded, win_year) * pct
1150    )
1151
1152    d = today - relativedelta(months=59)
1153    metrics["5Y (ann.) %"] = (
1154        _stats.cagr(df[df.index >= d], 0.0, compounded, win_year) * pct
1155    )
1156
1157    d = today - relativedelta(years=10)
1158    metrics["10Y (ann.) %"] = (
1159        _stats.cagr(df[df.index >= d], 0.0, compounded, win_year) * pct
1160    )
1161
1162    metrics["All-time (ann.) %"] = _stats.cagr(df, 0.0, compounded, win_year) * pct
1163
1164    # best/worst
1165    if mode.lower() == "full":
1166        metrics["~~~"] = blank
1167        metrics["Best Day %"] = (
1168            _stats.best(df, compounded=compounded, prepare_returns=False) * pct
1169        )
1170        metrics["Worst Day %"] = _stats.worst(df, prepare_returns=False) * pct
1171        metrics["Best Month %"] = (
1172            _stats.best(
1173                df, compounded=compounded, aggregate="ME", prepare_returns=False
1174            )
1175            * pct
1176        )
1177        metrics["Worst Month %"] = (
1178            _stats.worst(df, aggregate="ME", prepare_returns=False) * pct
1179        )
1180        metrics["Best Year %"] = (
1181            _stats.best(
1182                df, compounded=compounded, aggregate="YE", prepare_returns=False
1183            )
1184            * pct
1185        )
1186        metrics["Worst Year %"] = (
1187            _stats.worst(
1188                df, compounded=compounded, aggregate="YE", prepare_returns=False
1189            )
1190            * pct
1191        )
1192
1193    # dd
1194    metrics["~~~~"] = blank
1195    for ix, row in dd.iterrows():
1196        metrics[ix] = row
1197    metrics["Recovery Factor"] = _stats.recovery_factor(df)
1198    metrics["Ulcer Index"] = _stats.ulcer_index(df)
1199    metrics["Serenity Index"] = _stats.serenity_index(df, rf)
1200
1201    # win rate
1202    if mode.lower() == "full":
1203        metrics["~~~~~"] = blank
1204        metrics["Avg. Up Month %"] = (
1205            _stats.avg_win(
1206                df, compounded=compounded, aggregate="ME", prepare_returns=False
1207            )
1208            * pct
1209        )
1210        metrics["Avg. Down Month %"] = (
1211            _stats.avg_loss(
1212                df, compounded=compounded, aggregate="ME", prepare_returns=False
1213            )
1214            * pct
1215        )
1216
1217        # Get the win rate
1218        win_rate = _stats.win_rate(df, prepare_returns=False)
1219
1220        # Number of win days in total
1221        metrics["Win Days"] = win_rate * len(df)
1222
1223        # Number of loss days in total
1224        metrics["Loss Days"] = len(df) - metrics["Win Days"]
1225
1226        metrics["Win Days %%"] = win_rate * pct
1227        metrics["Win Month %%"] = (
1228            _stats.win_rate(
1229                df, compounded=compounded, aggregate="ME", prepare_returns=False
1230            )
1231            * pct
1232        )
1233        metrics["Win Quarter %%"] = (
1234            _stats.win_rate(
1235                df, compounded=compounded, aggregate="QE", prepare_returns=False
1236            )
1237            * pct
1238        )
1239        metrics["Win Year %%"] = (
1240            _stats.win_rate(
1241                df, compounded=compounded, aggregate="YE", prepare_returns=False
1242            )
1243            * pct
1244        )
1245
1246        if "benchmark" in df:
1247            metrics["~~~~~~~~~~~~"] = blank
1248            if isinstance(returns, _pd.Series):
1249                greeks = _stats.greeks(
1250                    df["returns"], df["benchmark"], win_year, prepare_returns=False
1251                )
1252                metrics["Beta"] = [str(round(greeks["beta"], 2)), "-"]
1253                metrics["Alpha"] = [str(round(greeks["alpha"], 2)), "-"]
1254                metrics["Correlation"] = [
1255                    str(round(df["benchmark"].corr(df["returns"]) * pct, 2)) + "%",
1256                    "-",
1257                ]
1258                metrics["Treynor Ratio"] = [
1259                    str(
1260                        round(
1261                            _stats.treynor_ratio(
1262                                df["returns"], df["benchmark"], win_year, rf
1263                            )
1264                            * pct,
1265                            2,
1266                        )
1267                    )
1268                    + "%",
1269                    "-",
1270                ]
1271            elif isinstance(returns, _pd.DataFrame):
1272                greeks = [
1273                    _stats.greeks(
1274                        df[strategy_col],
1275                        df["benchmark"],
1276                        win_year,
1277                        prepare_returns=False,
1278                    )
1279                    for strategy_col in df_strategy_columns
1280                ]
1281                metrics["Beta"] = [str(round(g["beta"], 2)) for g in greeks] + ["-"]
1282                metrics["Alpha"] = [str(round(g["alpha"], 2)) for g in greeks] + ["-"]
1283                metrics["Correlation"] = (
1284                    [
1285                        str(round(df["benchmark"].corr(df[strategy_col]) * pct, 2))
1286                        + "%"
1287                        for strategy_col in strategy_colname
1288                    ]
1289                ) + ["-"]
1290                metrics["Treynor Ratio"] = (
1291                    [
1292                        str(
1293                            round(
1294                                _stats.treynor_ratio(
1295                                    df[strategy_col], df["benchmark"], win_year, rf
1296                                )
1297                                * pct,
1298                                2,
1299                            )
1300                        )
1301                        + "%"
1302                        for strategy_col in strategy_colname
1303                    ]
1304                ) + ["-"]
1305
1306    # prepare for display
1307    for col in metrics.columns:
1308        try:
1309            metrics[col] = metrics[col].astype(float).round(2)
1310            if display or "internal" in kwargs:
1311                metrics[col] = metrics[col].astype(str)
1312        except Exception:
1313            pass
1314        if (display or "internal" in kwargs) and "*int" in col:
1315            metrics[col] = metrics[col].str.replace(".0", "", regex=False)
1316            metrics.rename({col: col.replace("*int", "")}, axis=1, inplace=True)
1317        if (display or "internal" in kwargs) and "%" in col:
1318            metrics[col] = metrics[col] + "%"
1319
1320    try:
1321        metrics["Longest DD Days"] = _pd.to_numeric(metrics["Longest DD Days"]).astype(
1322            "int"
1323        )
1324        metrics["Avg. Drawdown Days"] = _pd.to_numeric(
1325            metrics["Avg. Drawdown Days"]
1326        ).astype("int")
1327
1328        if display or "internal" in kwargs:
1329            metrics["Longest DD Days"] = metrics["Longest DD Days"].astype(str)
1330            metrics["Avg. Drawdown Days"] = metrics["Avg. Drawdown Days"].astype(str)
1331    except Exception:
1332        metrics["Longest DD Days"] = "-"
1333        metrics["Avg. Drawdown Days"] = "-"
1334        if display or "internal" in kwargs:
1335            metrics["Longest DD Days"] = "-"
1336            metrics["Avg. Drawdown Days"] = "-"
1337
1338    metrics.columns = [col if "~" not in col else "" for col in metrics.columns]
1339    metrics = metrics.T
1340
1341    if "benchmark" in df:
1342        column_names = [strategy_colname, benchmark_colname]
1343        if isinstance(strategy_colname, list):
1344            metrics.columns = list(_pd.core.common.flatten(column_names))
1345        else:
1346            metrics.columns = column_names
1347    else:
1348        if isinstance(strategy_colname, list):
1349            metrics.columns = strategy_colname
1350        else:
1351            metrics.columns = [strategy_colname]
1352
1353    # cleanups
1354    metrics.replace([-0, "-0"], 0, inplace=True)
1355    metrics.replace(
1356        [
1357            _np.nan,
1358            -_np.nan,
1359            _np.inf,
1360            -_np.inf,
1361            "-nan%",
1362            "nan%",
1363            "-nan",
1364            "nan",
1365            "-inf%",
1366            "inf%",
1367            "-inf",
1368            "inf",
1369        ],
1370        "-",
1371        inplace=True,
1372    )
1373
1374    # move benchmark to be the first column always if present
1375    if "benchmark" in df:
1376        metrics = metrics[
1377            [benchmark_colname]
1378            + [col for col in metrics.columns if col != benchmark_colname]
1379        ]
1380
1381    if display:
1382        print(_tabulate(metrics, headers="keys", tablefmt="simple"))
1383        return None
1384
1385    if not sep:
1386        metrics = metrics[metrics.index != ""]
1387
1388    # remove spaces from column names
1389    metrics = metrics.T
1390    metrics.columns = [
1391        c.replace(" %", "").replace(" *int", "").strip() for c in metrics.columns
1392    ]
1393    metrics = metrics.T
1394
1395    return metrics
1396
1397
1398def plots(
1399    returns,
1400    benchmark=None,
1401    grayscale=False,
1402    figsize=(8, 5),
1403    mode="basic",
1404    compounded=True,
1405    periods_per_year=365,
1406    prepare_returns=True,
1407    match_dates=True,
1408    **kwargs,
1409):
1410    """Plots for strategy performance"""
1411
1412    benchmark_colname = kwargs.get("benchmark_title", "Benchmark")
1413    strategy_colname = kwargs.get("strategy_title", "Strategy")
1414    active = kwargs.get("active", "False")
1415
1416    if (
1417        isinstance(returns, _pd.DataFrame)
1418        and len(returns.columns) > 1
1419        and isinstance(strategy_colname, str)
1420    ):
1421        strategy_colname = list(returns.columns)
1422
1423    win_year, win_half_year = _get_trading_periods(periods_per_year)
1424
1425    if match_dates is True:
1426        returns = returns.dropna()
1427
1428    if prepare_returns:
1429        returns = _utils._prepare_returns(returns)
1430
1431    if isinstance(returns, _pd.Series):
1432        returns.name = strategy_colname
1433    elif isinstance(returns, _pd.DataFrame):
1434        returns.columns = strategy_colname
1435
1436    if mode.lower() != "full":
1437        _plots.snapshot(
1438            returns,
1439            grayscale=grayscale,
1440            figsize=(figsize[0], figsize[0]),
1441            show=True,
1442            mode=("comp" if compounded else "sum"),
1443            benchmark_title=benchmark_colname,
1444            strategy_title=strategy_colname,
1445        )
1446
1447        if isinstance(returns, _pd.Series):
1448            _plots.monthly_heatmap(
1449                returns,
1450                benchmark,
1451                grayscale=grayscale,
1452                figsize=(figsize[0], figsize[0] * 0.5),
1453                show=True,
1454                ylabel=False,
1455                compounded=compounded,
1456                active=active,
1457            )
1458        elif isinstance(returns, _pd.DataFrame):
1459            for col in returns.columns:
1460                _plots.monthly_heatmap(
1461                    returns[col].dropna(),
1462                    benchmark,
1463                    grayscale=grayscale,
1464                    figsize=(figsize[0], figsize[0] * 0.5),
1465                    show=True,
1466                    ylabel=False,
1467                    returns_label=col,
1468                    compounded=compounded,
1469                    active=active,
1470                )
1471
1472        return
1473
1474    returns = _pd.DataFrame(returns)
1475
1476    # prepare timeseries
1477    if benchmark is not None:
1478        benchmark = _utils._prepare_benchmark(benchmark, returns.index)
1479        benchmark.name = benchmark_colname
1480        if match_dates is True:
1481            returns, benchmark = _match_dates(returns, benchmark)
1482
1483    _plots.returns(
1484        returns,
1485        benchmark,
1486        grayscale=grayscale,
1487        figsize=(figsize[0], figsize[0] * 0.6),
1488        show=True,
1489        ylabel=False,
1490        prepare_returns=False,
1491    )
1492
1493    _plots.log_returns(
1494        returns,
1495        benchmark,
1496        grayscale=grayscale,
1497        figsize=(figsize[0], figsize[0] * 0.5),
1498        show=True,
1499        ylabel=False,
1500        prepare_returns=False,
1501    )
1502
1503    if benchmark is not None:
1504        _plots.returns(
1505            returns,
1506            benchmark,
1507            match_volatility=True,
1508            grayscale=grayscale,
1509            figsize=(figsize[0], figsize[0] * 0.5),
1510            show=True,
1511            ylabel=False,
1512            prepare_returns=False,
1513        )
1514
1515    _plots.yearly_returns(
1516        returns,
1517        benchmark,
1518        grayscale=grayscale,
1519        figsize=(figsize[0], figsize[0] * 0.5),
1520        show=True,
1521        ylabel=False,
1522        prepare_returns=False,
1523    )
1524
1525    _plots.histogram(
1526        returns,
1527        benchmark,
1528        grayscale=grayscale,
1529        figsize=(figsize[0], figsize[0] * 0.5),
1530        show=True,
1531        ylabel=False,
1532        prepare_returns=False,
1533    )
1534
1535    small_fig_size = (figsize[0], figsize[0] * 0.35)
1536    if len(returns.columns) > 1:
1537        small_fig_size = (
1538            figsize[0],
1539            figsize[0] * (0.33 * (len(returns.columns) * 0.66)),
1540        )
1541
1542    _plots.daily_returns(
1543        returns,
1544        benchmark,
1545        grayscale=grayscale,
1546        figsize=small_fig_size,
1547        show=True,
1548        ylabel=False,
1549        prepare_returns=False,
1550        active=active,
1551    )
1552
1553    if benchmark is not None:
1554        _plots.rolling_beta(
1555            returns,
1556            benchmark,
1557            grayscale=grayscale,
1558            window1=win_half_year,
1559            window2=win_year,
1560            figsize=small_fig_size,
1561            show=True,
1562            ylabel=False,
1563            prepare_returns=False,
1564        )
1565
1566    _plots.rolling_volatility(
1567        returns,
1568        benchmark,
1569        grayscale=grayscale,
1570        figsize=small_fig_size,
1571        show=True,
1572        ylabel=False,
1573        period=win_half_year,
1574    )
1575
1576    _plots.rolling_sharpe(
1577        returns,
1578        grayscale=grayscale,
1579        figsize=small_fig_size,
1580        show=True,
1581        ylabel=False,
1582        period=win_half_year,
1583    )
1584
1585    _plots.rolling_sortino(
1586        returns,
1587        grayscale=grayscale,
1588        figsize=small_fig_size,
1589        show=True,
1590        ylabel=False,
1591        period=win_half_year,
1592    )
1593
1594    if isinstance(returns, _pd.Series):
1595        _plots.drawdowns_periods(
1596            returns,
1597            grayscale=grayscale,
1598            figsize=(figsize[0], figsize[0] * 0.5),
1599            show=True,
1600            ylabel=False,
1601            prepare_returns=False,
1602        )
1603    elif isinstance(returns, _pd.DataFrame):
1604        for col in returns.columns:
1605            _plots.drawdowns_periods(
1606                returns[col],
1607                grayscale=grayscale,
1608                figsize=(figsize[0], figsize[0] * 0.5),
1609                show=True,
1610                ylabel=False,
1611                title=col,
1612                prepare_returns=False,
1613            )
1614
1615    _plots.drawdown(
1616        returns,
1617        grayscale=grayscale,
1618        figsize=(figsize[0], figsize[0] * 0.4),
1619        show=True,
1620        ylabel=False,
1621    )
1622
1623    if isinstance(returns, _pd.Series):
1624        _plots.monthly_heatmap(
1625            returns,
1626            benchmark,
1627            grayscale=grayscale,
1628            figsize=(figsize[0], figsize[0] * 0.5),
1629            returns_label=returns.name,
1630            show=True,
1631            ylabel=False,
1632            active=active,
1633        )
1634    elif isinstance(returns, _pd.DataFrame):
1635        for col in returns.columns:
1636            _plots.monthly_heatmap(
1637                returns[col],
1638                benchmark,
1639                grayscale=grayscale,
1640                figsize=(figsize[0], figsize[0] * 0.5),
1641                show=True,
1642                ylabel=False,
1643                returns_label=col,
1644                compounded=compounded,
1645                active=active,
1646            )
1647
1648    if isinstance(returns, _pd.Series):
1649        _plots.distribution(
1650            returns,
1651            grayscale=grayscale,
1652            figsize=(figsize[0], figsize[0] * 0.5),
1653            show=True,
1654            title=returns.name,
1655            ylabel=False,
1656            prepare_returns=False,
1657        )
1658    elif isinstance(returns, _pd.DataFrame):
1659        for col in returns.columns:
1660            _plots.distribution(
1661                returns[col],
1662                grayscale=grayscale,
1663                figsize=(figsize[0], figsize[0] * 0.5),
1664                show=True,
1665                title=col,
1666                ylabel=False,
1667                prepare_returns=False,
1668            )
1669
1670
1671def _calc_dd(df, display=True, as_pct=False):
1672    """Returns drawdown stats"""
1673    dd = _stats.to_drawdown_series(df)
1674    dd_info = _stats.drawdown_details(dd)
1675
1676    if dd_info.empty:
1677        return _pd.DataFrame()
1678
1679    if "returns" in dd_info:
1680        ret_dd = dd_info["returns"]
1681    # to match multiple columns like returns_1, returns_2, ...
1682    elif (
1683        any(dd_info.columns.get_level_values(0).str.contains("returns"))
1684        and dd_info.columns.get_level_values(0).nunique() > 1
1685    ):
1686        ret_dd = dd_info.loc[
1687            :, dd_info.columns.get_level_values(0).str.contains("returns")
1688        ]
1689    else:
1690        ret_dd = dd_info
1691
1692    if (
1693        any(ret_dd.columns.get_level_values(0).str.contains("returns"))
1694        and ret_dd.columns.get_level_values(0).nunique() > 1
1695    ):
1696        dd_stats = {
1697            col: {
1698                "Max Drawdown %": ret_dd[col]
1699                .sort_values(by="max drawdown", ascending=True)["max drawdown"]
1700                .values[0]
1701                / 100,
1702                "Longest DD Days": str(
1703                    _np.round(
1704                        ret_dd[col]
1705                        .sort_values(by="days", ascending=False)["days"]
1706                        .values[0]
1707                    )
1708                ),
1709                "Avg. Drawdown %": ret_dd[col]["max drawdown"].mean() / 100,
1710                "Avg. Drawdown Days": str(_np.round(ret_dd[col]["days"].mean())),
1711            }
1712            for col in ret_dd.columns.get_level_values(0)
1713        }
1714    else:
1715        dd_stats = {
1716            "returns": {
1717                "Max Drawdown %": ret_dd.sort_values(by="max drawdown", ascending=True)[
1718                    "max drawdown"
1719                ].values[0]
1720                / 100,
1721                "Longest DD Days": str(
1722                    _np.round(
1723                        ret_dd.sort_values(by="days", ascending=False)["days"].values[0]
1724                    )
1725                ),
1726                "Avg. Drawdown %": ret_dd["max drawdown"].mean() / 100,
1727                "Avg. Drawdown Days": str(_np.round(ret_dd["days"].mean())),
1728            }
1729        }
1730    if "benchmark" in df and (dd_info.columns, _pd.MultiIndex):
1731        bench_dd = dd_info["benchmark"].sort_values(by="max drawdown")
1732        dd_stats["benchmark"] = {
1733            "Max Drawdown %": bench_dd.sort_values(by="max drawdown", ascending=True)[
1734                "max drawdown"
1735            ].values[0]
1736            / 100,
1737            "Longest DD Days": str(
1738                _np.round(
1739                    bench_dd.sort_values(by="days", ascending=False)["days"].values[0]
1740                )
1741            ),
1742            "Avg. Drawdown %": bench_dd["max drawdown"].mean() / 100,
1743            "Avg. Drawdown Days": str(_np.round(bench_dd["days"].mean())),
1744        }
1745
1746    # pct multiplier
1747    pct = 100 if display or as_pct else 1
1748
1749    dd_stats = _pd.DataFrame(dd_stats).T
1750    dd_stats["Max Drawdown %"] = dd_stats["Max Drawdown %"].astype(float) * pct
1751    dd_stats["Avg. Drawdown %"] = dd_stats["Avg. Drawdown %"].astype(float) * pct
1752
1753    return dd_stats.T
1754
1755
1756def _html_table(obj, showindex="default"):
1757    """Returns HTML table"""
1758    obj = _tabulate(
1759        obj, headers="keys", tablefmt="html", floatfmt=".2f", showindex=showindex
1760    )
1761    obj = obj.replace(' style="text-align: right;"', "")
1762    obj = obj.replace(' style="text-align: left;"', "")
1763    obj = obj.replace(' style="text-align: center;"', "")
1764    obj = _regex.sub("<td> +", "<td>", obj)
1765    obj = _regex.sub(" +</td>", "</td>", obj)
1766    obj = _regex.sub("<th> +", "<th>", obj)
1767    obj = _regex.sub(" +</th>", "</th>", obj)
1768    return obj
1769
1770
1771def _download_html(html, filename="quantstats-tearsheet.html"):
1772    """Downloads HTML report"""
1773    jscode = _regex.sub(
1774        " +",
1775        " ",
1776        """<script>
1777    var bl=new Blob(['{{html}}'],{type:"text/html"});
1778    var a=document.createElement("a");
1779    a.href=URL.createObjectURL(bl);
1780    a.download="{{filename}}";
1781    a.hidden=true;document.body.appendChild(a);
1782    a.innerHTML="download report";
1783    a.click();</script>""".replace("\n", ""),
1784    )
1785    jscode = jscode.replace("{{html}}", _regex.sub(" +", " ", html.replace("\n", "")))
1786    if _utils._in_notebook():
1787        iDisplay(iHTML(jscode.replace("{{filename}}", filename)))
1788
1789
1790def _open_html(html):
1791    """Opens HTML in a new tab"""
1792    jscode = _regex.sub(
1793        " +",
1794        " ",
1795        """<script>
1796    var win=window.open();win.document.body.innerHTML='{{html}}';
1797    </script>""".replace("\n", ""),
1798    )
1799    jscode = jscode.replace("{{html}}", _regex.sub(" +", " ", html.replace("\n", "")))
1800    if _utils._in_notebook():
1801        iDisplay(iHTML(jscode))
1802
1803
1804def _embed_figure(figfiles, figfmt):
1805    """Embeds the figure bytes in the html output"""
1806    if isinstance(figfiles, list):
1807        embed_string = "\n"
1808        for figfile in figfiles:
1809            figbytes = figfile.getvalue()
1810            if figfmt == "svg":
1811                return figbytes.decode()
1812            data_uri = _b64encode(figbytes).decode()
1813            embed_string.join(
1814                '<img src="data:image/{};base64,{}" />'.format(figfmt, data_uri)
1815            )
1816    else:
1817        figbytes = figfiles.getvalue()
1818        if figfmt == "svg":
1819            return figbytes.decode()
1820        data_uri = _b64encode(figbytes).decode()
1821        embed_string = '<img src="data:image/{};base64,{}" />'.format(figfmt, data_uri)
1822    return embed_string
def html( returns, benchmark: pandas.core.series.Series = None, rf: float = 0.0, grayscale: bool = False, title: str = 'Strategy Tearsheet', output: str = None, compounded: bool = True, periods_per_year: int = 365, download_filename: str = 'tearsheet.html', figfmt: str = 'svg', template_path: str = None, match_dates: bool = True, parameters: dict = None, log_scale: bool = False, show_match_volatility: bool = True, **kwargs):
 63def html(
 64    returns,
 65    benchmark: _pd.Series = None,
 66    rf: float = 0.0,
 67    grayscale: bool = False,
 68    title: str = "Strategy Tearsheet",
 69    output: str = None,
 70    compounded: bool = True,
 71    periods_per_year: int = 365,
 72    download_filename: str = "tearsheet.html",
 73    figfmt: str = "svg",
 74    template_path: str = None,
 75    match_dates: bool = True,
 76    parameters: dict = None,
 77    log_scale: bool = False,
 78    show_match_volatility: bool = True,
 79    **kwargs,
 80):
 81    """
 82    Generates a full HTML tearsheet with performance metrics and plots
 83
 84    Parameters
 85    ----------
 86    returns : pd.Series, pd.DataFrame
 87        Strategy returns
 88    benchmark : pd.Series, optional
 89        Benchmark returns
 90    rf : float, optional
 91        Risk-free rate, default is 0
 92    grayscale : bool, optional
 93        Plot in grayscale, default is False
 94    title : str, optional
 95        Title of the HTML report, default is "Strategy Tearsheet"
 96    output : str, optional
 97        Output file path
 98    compounded : bool, optional
 99        Whether to use compounded returns, default is True
100    periods_per_year : int, optional
101        Trading periods per year, default is 365
102    download_filename : str, optional
103        Download filename, default is "tearsheet.html"
104    figfmt : str, optional
105        Figure format, default is "svg"
106    template_path : str, optional
107        Custom template path
108    match_dates : bool, optional
109        Match dates of returns and benchmark, default is True
110    parameters : dict, optional
111        Strategy parameters
112
113    Returns
114    -------
115    None
116    """
117
118    if output is None and not _utils._in_notebook():
119        raise ValueError("`output` must be specified")
120
121    if match_dates:
122        returns = returns.dropna()
123
124    win_year, win_half_year = _get_trading_periods(periods_per_year)
125
126    tpl = ""
127    with open(template_path or __file__[:-4] + ".html", encoding='utf-8') as f:
128        tpl = f.read()
129        f.close()
130
131    # prepare timeseries
132    if match_dates:
133        returns = returns.dropna()
134    returns = _utils._prepare_returns(returns)
135
136    strategy_title = kwargs.get("strategy_title", "Strategy")
137    if (
138        isinstance(returns, _pd.DataFrame)
139        and len(returns.columns) > 1
140        and isinstance(strategy_title, str)
141    ):
142        strategy_title = list(returns.columns)
143
144    if benchmark is not None:
145        benchmark_title = kwargs.get("benchmark_title", "Benchmark")
146        if kwargs.get("benchmark_title") is None:
147            if isinstance(benchmark, str):
148                benchmark_title = benchmark
149            elif isinstance(benchmark, _pd.Series):
150                benchmark_title = benchmark.name
151            elif isinstance(benchmark, _pd.DataFrame):
152                benchmark_title = benchmark[benchmark.columns[0]].name
153
154        tpl = tpl.replace(
155            "{{benchmark_title}}", f"Benchmark is {benchmark_title.upper()} | "
156        )
157        benchmark = _utils._prepare_benchmark(benchmark, returns.index, rf)
158        if match_dates is True:
159            returns, benchmark = _match_dates(returns, benchmark)
160    else:
161        benchmark_title = None
162
163    # Assign names/columns after all preparations and matching
164    if benchmark is not None:
165        benchmark.name = benchmark_title
166    if isinstance(returns, _pd.Series):
167        returns.name = strategy_title
168    elif isinstance(returns, _pd.DataFrame):
169        returns.columns = strategy_title
170
171    # Check for no trades condition
172    no_trades_occurred = False
173    if returns.empty:
174        no_trades_occurred = True
175    else:
176        sum_abs_returns = 0.0
177        if isinstance(returns, _pd.Series):
178            # Assuming returns is numeric after _prepare_returns
179            sum_abs_returns = returns.abs().sum()
180        elif isinstance(returns, _pd.DataFrame):
181            # Assuming returns DataFrame columns are numeric after _prepare_returns
182            sum_abs_returns = returns.select_dtypes(include=[_np.number]).abs().sum().sum()
183        
184        if abs(sum_abs_returns) < 1e-9: # Using a small epsilon for float comparison
185            no_trades_occurred = True
186
187    no_trades_html_message = ""
188    if no_trades_occurred:
189        no_trades_html_message = "<div style='text-align: center; padding: 10px; background-color: #fff3cd; color: #856404; border: 1px solid #ffeeba; border-radius: .25rem; margin-bottom: 1rem;'><strong>Note:</strong> No trades or significant activity occurred during this period. Metrics shown below may reflect this.</div>"
190
191    date_range = returns.index.strftime("%e %b, %Y")
192    tpl = tpl.replace("{{date_range}}", date_range[0] + " - " + date_range[-1])
193    tpl = tpl.replace("{{title}}", title)
194    tpl = tpl.replace("{{v}}", __version__)
195
196    if benchmark is not None:
197        benchmark.name = benchmark_title
198    if isinstance(returns, _pd.Series):
199        returns.name = strategy_title
200    elif isinstance(returns, _pd.DataFrame):
201        returns.columns = strategy_title
202
203    mtrx = metrics(
204        returns=returns,
205        benchmark=benchmark,
206        rf=rf,
207        display=False,
208        mode="full",
209        sep=True,
210        internal="True",
211        compounded=compounded,
212        periods_per_year=periods_per_year,
213        prepare_returns=False,
214        benchmark_title=benchmark_title,
215        strategy_title=strategy_title,
216    )[2:]
217
218    mtrx.index.name = "Metric"
219    # tpl = tpl.replace("{{metrics}}", _html_table(mtrx)) # Original line
220    
221    # Modified replacement for metrics table
222    metrics_table_html = _html_table(mtrx)
223    # The "no trades" message is no longer prepended here.
224    tpl = tpl.replace("{{metrics}}", metrics_table_html, 1)
225
226    # Add all of the summary metrics
227    # Ensure these are cast to str and the no_trades_html_message is NOT with CAGR here.
228
229    # CAGR #
230    cagr_value = mtrx.loc["CAGR% (Annual Return)", strategy_title]
231    # Ensure CAGR is formatted as a percentage string with 2 decimals
232    if isinstance(cagr_value, float):
233        cagr_str = f"{cagr_value:.2f}%"
234    else:
235        cagr_str = str(cagr_value)
236    tpl = tpl.replace("{{cagr}}", cagr_str)
237
238    # Total Return #
239    total_return_value = mtrx.loc["Total Return", strategy_title]
240    # Ensure Total Return is formatted as a percentage string with 2 decimals
241    if isinstance(total_return_value, float):
242        total_return_str = f"{total_return_value:.2f}%"
243    else:
244        total_return_str = str(total_return_value)
245    tpl = tpl.replace("{{total_return}}", total_return_str)
246
247    # Max Drawdown #
248    max_drawdown_value = mtrx.loc["Max Drawdown", strategy_title]
249    if isinstance(max_drawdown_value, float):
250        max_drawdown_str = f"{max_drawdown_value:.2f}%"
251    else:
252        max_drawdown_str = str(max_drawdown_value)
253    tpl = tpl.replace("{{max_drawdown}}", max_drawdown_str)
254
255    # RoMaD #
256    romad_value = mtrx.loc["RoMaD", strategy_title]
257    if isinstance(romad_value, float):
258        romad_str = f"{romad_value:.2f}"
259    else:
260        romad_str = str(romad_value)
261    tpl = tpl.replace("{{romad}}", romad_str)
262
263    # Longest Drawdown Duration #
264    longest_dd_days_value = mtrx.loc["Longest DD Days", strategy_title]
265    tpl = tpl.replace("{{longest_dd_days}}", str(longest_dd_days_value))
266
267    # Sharpe #
268    sharpe_value = mtrx.loc["Sharpe", strategy_title]
269    if isinstance(sharpe_value, float):
270        sharpe_str = f"{sharpe_value:.2f}"
271    else:
272        sharpe_str = str(sharpe_value)
273    tpl = tpl.replace("{{sharpe}}", sharpe_str)
274
275    # Sortino #
276    sortino_value = mtrx.loc["Sortino", strategy_title]
277    if isinstance(sortino_value, float):
278        sortino_str = f"{sortino_value:.2f}"
279    else:
280        sortino_str = str(sortino_value)
281    tpl = tpl.replace("{{sortino}}", sortino_str)
282
283
284    if isinstance(returns, _pd.DataFrame):
285        num_cols = len(returns.columns)
286        for i in reversed(range(num_cols + 1, num_cols + 3)):
287            str_td = "<td></td>" * i
288            tpl = tpl.replace(
289                f"<tr>{str_td}</tr>", '<tr><td colspan="{}"><hr></td></tr>'.format(i)
290            )
291
292    tpl = tpl.replace(
293        "<tr><td></td><td></td><td></td></tr>", '<tr><td colspan="3"><hr></td></tr>'
294    )
295    tpl = tpl.replace(
296        "<tr><td></td><td></td></tr>", '<tr><td colspan="2"><hr></td></tr>'
297    )
298
299    if parameters is not None:
300        tpl = tpl.replace("{{parameters_section}}", parameters_section(parameters))
301
302    if benchmark is not None:
303        yoy = _stats.compare(
304            returns, benchmark, "YE", compounded=compounded, prepare_returns=False
305        )
306        if isinstance(returns, _pd.Series):
307            yoy.columns = [benchmark_title, strategy_title, "Multiplier", "Won"]
308        elif isinstance(returns, _pd.DataFrame):
309            yoy.columns = list(
310                _pd.core.common.flatten([benchmark_title, strategy_title])
311            )
312        yoy.index.name = "Year"
313        tpl = tpl.replace("{{eoy_title}}", "<h3>EOY Returns vs Benchmark</h3>")
314        tpl = tpl.replace("{{eoy_table}}", _html_table(yoy))
315    else:
316        # pct multiplier
317        yoy = _pd.DataFrame(_utils.group_returns(returns, returns.index.year) * 100)
318        if isinstance(returns, _pd.Series):
319            yoy.columns = ["Return"]
320            yoy["Cumulative"] = _utils.group_returns(returns, returns.index.year, True)
321            yoy["Return"] = yoy["Return"].round(2).astype(str) + "%"
322            yoy["Cumulative"] = (yoy["Cumulative"] * 100).round(2).astype(str) + "%"
323        elif isinstance(returns, _pd.DataFrame):
324            # Don't show cumulative for multiple strategy portfolios
325            # just show compounded like when we have a benchmark
326            yoy.columns = list(_pd.core.common.flatten(strategy_title))
327
328        yoy.index.name = "Year"
329        tpl = tpl.replace("{{eoy_title}}", "<h3>EOY Returns</h3>")
330        tpl = tpl.replace("{{eoy_table}}", _html_table(yoy))
331
332    if isinstance(returns, _pd.Series):
333        dd = _stats.to_drawdown_series(returns)
334        dd_info = _stats.drawdown_details(dd).sort_values(
335            by="max drawdown", ascending=True
336        )[:10]
337        dd_info = dd_info[["start", "end", "max drawdown", "days"]]
338        dd_info.columns = ["Started", "Recovered", "Drawdown", "Days"]
339        tpl = tpl.replace("{{dd_info}}", _html_table(dd_info, False))
340    elif isinstance(returns, _pd.DataFrame):
341        dd_info_list = []
342        for col in returns.columns:
343            dd = _stats.to_drawdown_series(returns[col])
344            dd_info = _stats.drawdown_details(dd).sort_values(
345                by="max drawdown", ascending=True
346            )[:10]
347            dd_info = dd_info[["start", "end", "max drawdown", "days"]]
348            dd_info.columns = ["Started", "Recovered", "Drawdown", "Days"]
349            dd_info_list.append(_html_table(dd_info, False))
350
351        dd_html_table = ""
352        for html_str, col in zip(dd_info_list, returns.columns):
353            dd_html_table = (
354                dd_html_table + f"<h3>{col}</h3><br>" + StringIO(html_str).read()
355            )
356        tpl = tpl.replace("{{dd_info}}", dd_html_table)
357
358    active = kwargs.get("active_returns", False)
359    # plots
360    plot_returns = _plots.log_returns if log_scale else _plots.returns
361    placeholder_returns = "{{log_returns}}" if log_scale else "{{returns}}"
362
363    figfile = _utils._file_stream()
364    plot_returns(
365        returns,
366        benchmark,
367        grayscale=grayscale,
368        figsize=(8, 5),
369        subtitle=False,
370        savefig={"fname": figfile, "format": figfmt},
371        show=False,
372        ylabel=False,
373        cumulative=compounded,
374        prepare_returns=False,
375    )
376    first_plot_html = _embed_figure(figfile, figfmt) # Get the HTML for the first plot
377
378    # Prepend the no_trades_html_message if no trades occurred, then add the plot
379    if no_trades_occurred:
380        tpl = tpl.replace(placeholder_returns, no_trades_html_message + first_plot_html, 1)
381    else:
382        tpl = tpl.replace(placeholder_returns, first_plot_html, 1)
383
384    if benchmark is not None and show_match_volatility:
385        figfile = _utils._file_stream()
386        plot_returns(
387            returns,
388            benchmark,
389            match_volatility=True,
390            grayscale=grayscale,
391            figsize=(8, 5),
392            subtitle=False,
393            savefig={"fname": figfile, "format": figfmt},
394            show=False,
395            ylabel=False,
396            cumulative=compounded,
397            prepare_returns=False,
398        )
399        tpl = tpl.replace("{{vol_returns}}", _embed_figure(figfile, figfmt))
400
401    figfile = _utils._file_stream()
402    _plots.yearly_returns(
403        returns,
404        benchmark,
405        grayscale=grayscale,
406        figsize=(8, 4),
407        subtitle=False,
408        savefig={"fname": figfile, "format": figfmt},
409        show=False,
410        ylabel=False,
411        compounded=compounded,
412        prepare_returns=False,
413    )
414    tpl = tpl.replace("{{eoy_returns}}", _embed_figure(figfile, figfmt))
415
416    figfile = _utils._file_stream()
417    _plots.histogram(
418        returns,
419        benchmark,
420        grayscale=grayscale,
421        figsize=(7, 4),
422        subtitle=False,
423        savefig={"fname": figfile, "format": figfmt},
424        show=False,
425        ylabel=False,
426        compounded=compounded,
427        prepare_returns=False,
428    )
429    tpl = tpl.replace("{{monthly_dist}}", _embed_figure(figfile, figfmt))
430
431    figfile = _utils._file_stream()
432    _plots.daily_returns(
433        returns,
434        benchmark,
435        grayscale=grayscale,
436        figsize=(8, 3),
437        subtitle=False,
438        savefig={"fname": figfile, "format": figfmt},
439        show=False,
440        ylabel=False,
441        prepare_returns=False,
442        active=active,
443    )
444    tpl = tpl.replace("{{daily_returns}}", _embed_figure(figfile, figfmt))
445
446    if benchmark is not None:
447        figfile = _utils._file_stream()
448        _plots.rolling_beta(
449            returns,
450            benchmark,
451            grayscale=grayscale,
452            figsize=(8, 3),
453            subtitle=False,
454            window1=win_half_year,
455            window2=win_year,
456            savefig={"fname": figfile, "format": figfmt},
457            show=False,
458            ylabel=False,
459            prepare_returns=False,
460        )
461        tpl = tpl.replace("{{rolling_beta}}", _embed_figure(figfile, figfmt))
462
463    figfile = _utils._file_stream()
464    _plots.rolling_volatility(
465        returns,
466        benchmark,
467        grayscale=grayscale,
468        figsize=(8, 3),
469        subtitle=False,
470        savefig={"fname": figfile, "format": figfmt},
471        show=False,
472        ylabel=False,
473        period=win_half_year,
474        periods_per_year=win_year,
475    )
476    tpl = tpl.replace("{{rolling_vol}}", _embed_figure(figfile, figfmt))
477
478    figfile = _utils._file_stream()
479    _plots.rolling_sharpe(
480        returns,
481        grayscale=grayscale,
482        figsize=(8, 3),
483        subtitle=False,
484        savefig={"fname": figfile, "format": figfmt},
485        show=False,
486        ylabel=False,
487        period=win_half_year,
488        periods_per_year=win_year,
489    )
490    tpl = tpl.replace("{{rolling_sharpe}}", _embed_figure(figfile, figfmt))
491
492    figfile = _utils._file_stream()
493    _plots.rolling_sortino(
494        returns,
495        grayscale=grayscale,
496        figsize=(8, 3),
497        subtitle=False,
498        savefig={"fname": figfile, "format": figfmt},
499        show=False,
500        ylabel=False,
501        period=win_half_year,
502        periods_per_year=win_year,
503    )
504    tpl = tpl.replace("{{rolling_sortino}}", _embed_figure(figfile, figfmt))
505
506    figfile = _utils._file_stream()
507    if isinstance(returns, _pd.Series):
508        _plots.drawdowns_periods(
509            returns,
510            grayscale=grayscale,
511            figsize=(8, 4),
512            subtitle=False,
513            title=returns.name,
514            savefig={"fname": figfile, "format": figfmt},
515            show=False,
516            ylabel=False,
517            compounded=compounded,
518            prepare_returns=False,
519            log_scale=log_scale,
520        )
521        tpl = tpl.replace("{{dd_periods}}", _embed_figure(figfile, figfmt))
522    elif isinstance(returns, _pd.DataFrame):
523        embed = []
524        for col in returns.columns:
525            _plots.drawdowns_periods(
526                returns[col],
527                grayscale=grayscale,
528                figsize=(8, 4),
529                subtitle=False,
530                title=col,
531                savefig={"fname": figfile, "format": figfmt},
532                show=False,
533                ylabel=False,
534                compounded=compounded,
535                prepare_returns=False,
536            )
537            embed.append(figfile)
538        tpl = tpl.replace("{{dd_periods}}", _embed_figure(embed, figfmt))
539
540    figfile = _utils._file_stream()
541    _plots.drawdown(
542        returns,
543        grayscale=grayscale,
544        figsize=(8, 3),
545        subtitle=False,
546        savefig={"fname": figfile, "format": figfmt},
547        show=False,
548        ylabel=False,
549    )
550    tpl = tpl.replace("{{dd_plot}}", _embed_figure(figfile, figfmt))
551
552    figfile = _utils._file_stream()
553    if isinstance(returns, _pd.Series):
554        _plots.monthly_heatmap(
555            returns,
556            benchmark,
557            grayscale=grayscale,
558            figsize=(8, 4),
559            cbar=False,
560            returns_label=returns.name,
561            savefig={"fname": figfile, "format": figfmt},
562            show=False,
563            ylabel=False,
564            compounded=compounded,
565            active=active,
566        )
567        tpl = tpl.replace("{{monthly_heatmap}}", _embed_figure(figfile, figfmt))
568    elif isinstance(returns, _pd.DataFrame):
569        embed = []
570        for col in returns.columns:
571            _plots.monthly_heatmap(
572                returns[col],
573                benchmark,
574                grayscale=grayscale,
575                figsize=(8, 4),
576                cbar=False,
577                returns_label=col,
578                savefig={"fname": figfile, "format": figfmt},
579                show=False,
580                ylabel=False,
581                compounded=compounded,
582                active=active,
583            )
584            embed.append(figfile)
585        tpl = tpl.replace("{{monthly_heatmap}}", _embed_figure(embed, figfmt))
586
587    figfile = _utils._file_stream()
588
589    if isinstance(returns, _pd.Series):
590        _plots.distribution(
591            returns,
592            grayscale=grayscale,
593            figsize=(8, 4),
594            subtitle=False,
595            title=returns.name,
596            savefig={"fname": figfile, "format": figfmt},
597            show=False,
598            ylabel=False,
599            compounded=compounded,
600            prepare_returns=False,
601        )
602        tpl = tpl.replace("{{returns_dist}}", _embed_figure(figfile, figfmt))
603    elif isinstance(returns, _pd.DataFrame):
604        embed = []
605        for col in returns.columns:
606            _plots.distribution(
607                returns[col],
608                grayscale=grayscale,
609                figsize=(8, 4),
610                subtitle=False,
611                title=col,
612                savefig={"fname": figfile, "format": figfmt},
613                show=False,
614                ylabel=False,
615                compounded=compounded,
616                prepare_returns=False,
617            )
618            embed.append(figfile)
619        tpl = tpl.replace("{{returns_dist}}", _embed_figure(embed, figfmt))
620
621    tpl = _regex.sub(r"\{\{(.*?)\}\}", "", tpl)
622    tpl = tpl.replace("white-space:pre;", "")
623
624    if output is None:
625        # _open_html(tpl)
626        _download_html(tpl, download_filename)
627        return
628
629    with open(output, "w", encoding="utf-8") as f:
630        f.write(tpl)
631
632    print(f"HTML report saved to: {output}")
633
634    # Return the metrics
635    return mtrx

Generates a full HTML tearsheet with performance metrics and plots

Parameters

returns : pd.Series, pd.DataFrame Strategy returns benchmark : pd.Series, optional Benchmark returns rf : float, optional Risk-free rate, default is 0 grayscale : bool, optional Plot in grayscale, default is False title : str, optional Title of the HTML report, default is "Strategy Tearsheet" output : str, optional Output file path compounded : bool, optional Whether to use compounded returns, default is True periods_per_year : int, optional Trading periods per year, default is 365 download_filename : str, optional Download filename, default is "tearsheet.html" figfmt : str, optional Figure format, default is "svg" template_path : str, optional Custom template path match_dates : bool, optional Match dates of returns and benchmark, default is True parameters : dict, optional Strategy parameters

Returns

None

def full( returns, benchmark=None, rf=0.0, grayscale=False, figsize=(8, 5), display=True, compounded=True, periods_per_year=365, match_dates=True, **kwargs):
638def full(
639    returns,
640    benchmark=None,
641    rf=0.0,
642    grayscale=False,
643    figsize=(8, 5),
644    display=True,
645    compounded=True,
646    periods_per_year=365,
647    match_dates=True,
648    **kwargs,
649):
650    """calculates and plots full performance metrics"""
651
652    # prepare timeseries
653    if match_dates:
654        returns = returns.dropna()
655    returns = _utils._prepare_returns(returns)
656    if benchmark is not None:
657        benchmark = _utils._prepare_benchmark(benchmark, returns.index, rf)
658        if match_dates is True:
659            returns, benchmark = _match_dates(returns, benchmark)
660
661    benchmark_title = None
662    if benchmark is not None:
663        benchmark_title = kwargs.get("benchmark_title", "Benchmark")
664    strategy_title = kwargs.get("strategy_title", "Strategy")
665    active = kwargs.get("active_returns", False)
666
667    if (
668        isinstance(returns, _pd.DataFrame)
669        and len(returns.columns) > 1
670        and isinstance(strategy_title, str)
671    ):
672        strategy_title = list(returns.columns)
673
674    if benchmark is not None:
675        benchmark.name = benchmark_title
676    if isinstance(returns, _pd.Series):
677        returns.name = strategy_title
678    elif isinstance(returns, _pd.DataFrame):
679        returns.columns = strategy_title
680
681    dd = _stats.to_drawdown_series(returns)
682
683    if isinstance(dd, _pd.Series):
684        col = _stats.drawdown_details(dd).columns[4]
685        dd_info = _stats.drawdown_details(dd).sort_values(by=col, ascending=True)[:5]
686        if not dd_info.empty:
687            dd_info.index = range(1, min(6, len(dd_info) + 1))
688            dd_info.columns = map(lambda x: str(x).title(), dd_info.columns)
689    elif isinstance(dd, _pd.DataFrame):
690        col = _stats.drawdown_details(dd).columns.get_level_values(1)[4]
691        dd_info_dict = {}
692        for ptf in dd.columns:
693            dd_info = _stats.drawdown_details(dd[ptf]).sort_values(
694                by=col, ascending=True
695            )[:5]
696            if not dd_info.empty:
697                dd_info.index = range(1, min(6, len(dd_info) + 1))
698                dd_info.columns = map(lambda x: str(x).title(), dd_info.columns)
699            dd_info_dict[ptf] = dd_info
700
701    if _utils._in_notebook():
702        iDisplay(iHTML("<h4>Performance Metrics</h4>"))
703        iDisplay(
704            metrics(
705                returns=returns,
706                benchmark=benchmark,
707                rf=rf,
708                display=display,
709                mode="full",
710                compounded=compounded,
711                periods_per_year=periods_per_year,
712                prepare_returns=False,
713                benchmark_title=benchmark_title,
714                strategy_title=strategy_title,
715            )
716        )
717
718        if isinstance(dd, _pd.Series):
719            iDisplay(iHTML('<h4 style="margin-bottom:20px">Worst 5 Drawdowns</h4>'))
720            if dd_info.empty:
721                iDisplay(iHTML("<p>(no drawdowns)</p>"))
722            else:
723                iDisplay(dd_info)
724        elif isinstance(dd, _pd.DataFrame):
725            for ptf, dd_info in dd_info_dict.items():
726                iDisplay(
727                    iHTML(
728                        '<h4 style="margin-bottom:20px">%s - Worst 5 Drawdowns</h4>'
729                        % ptf
730                    )
731                )
732                if dd_info.empty:
733                    iDisplay(iHTML("<p>(no drawdowns)</p>"))
734                else:
735                    iDisplay(dd_info)
736
737        iDisplay(iHTML("<h4>Strategy Visualization</h4>"))
738    else:
739        print("[Performance Metrics]\n")
740        metrics(
741            returns=returns,
742            benchmark=benchmark,
743            rf=rf,
744            display=display,
745            mode="full",
746            compounded=compounded,
747            periods_per_year=periods_per_year,
748            prepare_returns=False,
749            benchmark_title=benchmark_title,
750            strategy_title=strategy_title,
751        )
752        print("\n\n")
753        print("[Worst 5 Drawdowns]\n")
754        if isinstance(dd, _pd.Series):
755            if dd_info.empty:
756                print("(no drawdowns)")
757            else:
758                print(
759                    _tabulate(
760                        dd_info, headers="keys", tablefmt="simple", floatfmt=".2f"
761                    )
762                )
763        elif isinstance(dd, _pd.DataFrame):
764            for ptf, dd_info in dd_info_dict.items():
765                if dd_info.empty:
766                    print("(no drawdowns)")
767                else:
768                    print(f"{ptf}\n")
769                    print(
770                        _tabulate(
771                            dd_info, headers="keys", tablefmt="simple", floatfmt=".2f"
772                        )
773                    )
774
775        print("\n\n")
776        print("[Strategy Visualization]\nvia Matplotlib")
777
778    plots(
779        returns=returns,
780        benchmark=benchmark,
781        grayscale=grayscale,
782        figsize=figsize,
783        mode="full",
784        periods_per_year=periods_per_year,
785        prepare_returns=False,
786        benchmark_title=benchmark_title,
787        strategy_title=strategy_title,
788        active=active,
789    )

calculates and plots full performance metrics

def basic( returns, benchmark=None, rf=0.0, grayscale=False, figsize=(8, 5), display=True, compounded=True, periods_per_year=365, match_dates=True, **kwargs):
791def basic(
792    returns,
793    benchmark=None,
794    rf=0.0,
795    grayscale=False,
796    figsize=(8, 5),
797    display=True,
798    compounded=True,
799    periods_per_year=365,
800    match_dates=True,
801    **kwargs,
802):
803    """calculates and plots basic performance metrics"""
804
805    # prepare timeseries
806    if match_dates:
807        returns = returns.dropna()
808    returns = _utils._prepare_returns(returns)
809    if benchmark is not None:
810        benchmark = _utils._prepare_benchmark(benchmark, returns.index, rf)
811        if match_dates is True:
812            returns, benchmark = _match_dates(returns, benchmark)
813
814    benchmark_title = None
815    if benchmark is not None:
816        benchmark_title = kwargs.get("benchmark_title", "Benchmark")
817    strategy_title = kwargs.get("strategy_title", "Strategy")
818    active = kwargs.get("active_returns", False)
819
820    if (
821        isinstance(returns, _pd.DataFrame)
822        and len(returns.columns) > 1
823        and isinstance(strategy_title, str)
824    ):
825        strategy_title = list(returns.columns)
826
827    if _utils._in_notebook():
828        iDisplay(iHTML("<h4>Performance Metrics</h4>"))
829        metrics(
830            returns=returns,
831            benchmark=benchmark,
832            rf=rf,
833            display=display,
834            mode="basic",
835            compounded=compounded,
836            periods_per_year=periods_per_year,
837            prepare_returns=False,
838            benchmark_title=benchmark_title,
839            strategy_title=strategy_title,
840        )
841        iDisplay(iHTML("<h4>Strategy Visualization</h4>"))
842    else:
843        print("[Performance Metrics]\n")
844        metrics(
845            returns=returns,
846            benchmark=benchmark,
847            rf=rf,
848            display=display,
849            mode="basic",
850            compounded=compounded,
851            periods_per_year=periods_per_year,
852            prepare_returns=False,
853            benchmark_title=benchmark_title,
854            strategy_title=strategy_title,
855        )
856
857        print("\n\n")
858        print("[Strategy Visualization]\nvia Matplotlib")
859
860    plots(
861        returns=returns,
862        benchmark=benchmark,
863        grayscale=grayscale,
864        figsize=figsize,
865        mode="basic",
866        periods_per_year=periods_per_year,
867        prepare_returns=False,
868        benchmark_title=benchmark_title,
869        strategy_title=strategy_title,
870        active=active,
871    )

calculates and plots basic performance metrics

def parameters_section(parameters):
873def parameters_section(parameters):
874    """returns a formatted section for strategy parameters"""
875    if parameters is None:
876        return ""
877
878    tpl = """
879    <div id="params">
880        <h3>Parameters Used</h3>
881        <table style="width:100%; font-size: 12px">
882    """
883
884    # Add titles to the table
885    tpl += "<thead><tr><th>Parameter</th><th>Value</th></tr></thead>"
886
887    for key, value in parameters.items():
888        # Make sure that the value is something that can be displayed
889        if not isinstance(value, (int, float, str)):
890            value = str(value)
891
892        tpl += f"<tr><td>{key}</td><td>{value}</td></tr>"
893    tpl += """
894        </table>
895    </div>
896    """
897
898    return tpl

returns a formatted section for strategy parameters

def metrics( returns, benchmark=None, rf=0.0, display=True, mode='basic', sep=False, compounded=True, periods_per_year=365, prepare_returns=True, match_dates=True, **kwargs):
 900def metrics(
 901    returns,
 902    benchmark=None,
 903    rf=0.0,
 904    display=True,
 905    mode="basic",
 906    sep=False,
 907    compounded=True,
 908    periods_per_year=365,
 909    prepare_returns=True,
 910    match_dates=True,
 911    **kwargs,
 912):
 913    """calculates and displays various performance metrics"""
 914
 915    if match_dates:
 916        returns = returns.dropna()
 917    returns.index = returns.index.tz_localize(None)
 918    win_year, _ = _get_trading_periods(periods_per_year)
 919
 920    benchmark_colname = kwargs.get("benchmark_title", "Benchmark")
 921    strategy_colname = kwargs.get("strategy_title", "Strategy")
 922
 923    if benchmark is not None:
 924        if isinstance(benchmark, str):
 925            benchmark_colname = f"Benchmark ({benchmark.upper()})"
 926        elif isinstance(benchmark, _pd.DataFrame) and len(benchmark.columns) > 1:
 927            raise ValueError(
 928                "`benchmark` must be a pandas Series, "
 929                "but a multi-column DataFrame was passed"
 930            )
 931
 932    if isinstance(returns, _pd.DataFrame):
 933        if len(returns.columns) > 1:
 934            blank = [""] * len(returns.columns)
 935            if isinstance(strategy_colname, str):
 936                strategy_colname = list(returns.columns)
 937    else:
 938        blank = [""]
 939
 940    if prepare_returns:
 941        df = _utils._prepare_returns(returns)
 942
 943    if isinstance(returns, _pd.Series):
 944        df = _pd.DataFrame({"returns": returns})
 945    elif isinstance(returns, _pd.DataFrame):
 946        df = _pd.DataFrame(
 947            {
 948                "returns_" + str(i + 1): returns[strategy_col]
 949                for i, strategy_col in enumerate(returns.columns)
 950            }
 951        )
 952
 953    if benchmark is not None:
 954        benchmark = _utils._prepare_benchmark(benchmark, returns.index, rf)
 955        if match_dates is True:
 956            returns, benchmark = _match_dates(returns, benchmark)
 957        df["benchmark"] = benchmark
 958        if isinstance(returns, _pd.Series):
 959            blank = ["", ""]
 960            df["returns"] = returns
 961        elif isinstance(returns, _pd.DataFrame):
 962            blank = [""] * len(returns.columns) + [""]
 963            for i, strategy_col in enumerate(returns.columns):
 964                df["returns_" + str(i + 1)] = returns[strategy_col]
 965
 966    if isinstance(returns, _pd.Series):
 967        s_start = {"returns": df["returns"].index.strftime("%Y-%m-%d")[0]}
 968        s_end = {"returns": df["returns"].index.strftime("%Y-%m-%d")[-1]}
 969        s_rf = {"returns": rf}
 970    elif isinstance(returns, _pd.DataFrame):
 971        df_strategy_columns = [col for col in df.columns if col != "benchmark"]
 972        s_start = {
 973            strategy_col: df[strategy_col].dropna().index.strftime("%Y-%m-%d")[0]
 974            for strategy_col in df_strategy_columns
 975        }
 976        s_end = {
 977            strategy_col: df[strategy_col].dropna().index.strftime("%Y-%m-%d")[-1]
 978            for strategy_col in df_strategy_columns
 979        }
 980        s_rf = {strategy_col: rf for strategy_col in df_strategy_columns}
 981
 982    if "benchmark" in df:
 983        s_start["benchmark"] = df["benchmark"].index.strftime("%Y-%m-%d")[0]
 984        s_end["benchmark"] = df["benchmark"].index.strftime("%Y-%m-%d")[-1]
 985        s_rf["benchmark"] = rf
 986
 987    df = df.fillna(0)
 988
 989    # pct multiplier
 990    pct = 100 if display or "internal" in kwargs else 1
 991    if kwargs.get("as_pct", False):
 992        pct = 100
 993
 994    # return df
 995    dd = _calc_dd(
 996        df,
 997        display=(display or "internal" in kwargs),
 998        as_pct=kwargs.get("as_pct", False),
 999    )
1000
1001    metrics = _pd.DataFrame()
1002    metrics["Start Period"] = _pd.Series(s_start)
1003    metrics["End Period"] = _pd.Series(s_end)
1004    metrics["Risk-Free Rate % "] = _pd.Series(s_rf) * 100
1005    metrics["Time in Market % "] = _stats.exposure(df, prepare_returns=False) * pct
1006
1007    metrics["~"] = blank
1008
1009    if compounded:
1010        metrics["Total Return"] = (_stats.comp(df) * pct).map("{:,.0f}%".format)  # No decimals for readability as it is a large number
1011    else:
1012        metrics["Total Return"] = (df.sum() * pct).map("{:,.2f}%".format)
1013
1014    metrics["CAGR% (Annual Return) "] = _stats.cagr(df, rf, compounded, win_year) * pct
1015
1016    metrics["~~~~~~~~~~~~~~"] = blank
1017
1018    metrics["Sharpe"] = _stats.sharpe(df, rf, win_year, True)
1019    metrics["RoMaD"] = _stats.romad(df, win_year, True)
1020
1021    if benchmark is not None:
1022        metrics["Corr to Benchmark "] = _stats.benchmark_correlation(df, benchmark, True)
1023    metrics["Prob. Sharpe Ratio %"] = (
1024        _stats.probabilistic_sharpe_ratio(df, rf, win_year, False) * pct
1025    )
1026    if mode.lower() == "full":
1027        metrics["Smart Sharpe"] = _stats.smart_sharpe(df, rf, win_year, True)
1028
1029    metrics["Sortino"] = _stats.sortino(df, rf, win_year, True)
1030    if mode.lower() == "full":
1031        metrics["Smart Sortino"] = _stats.smart_sortino(df, rf, win_year, True)
1032
1033    metrics["Sortino/√2"] = metrics["Sortino"] / _sqrt(2)
1034    if mode.lower() == "full":
1035        metrics["Smart Sortino/√2"] = metrics["Smart Sortino"] / _sqrt(2)
1036    metrics["Omega"] = _stats.omega(df, rf, 0.0, win_year)
1037
1038    metrics["~~~~~~~~"] = blank
1039    metrics["Max Drawdown %"] = blank
1040    metrics["Longest DD Days"] = blank
1041
1042    if mode.lower() == "full":
1043        if isinstance(returns, _pd.Series):
1044            ret_vol = (
1045                _stats.volatility(df["returns"], win_year, True, prepare_returns=False)
1046                * pct
1047            )
1048        elif isinstance(returns, _pd.DataFrame):
1049            ret_vol = [
1050                _stats.volatility(
1051                    df[strategy_col], win_year, True, prepare_returns=False
1052                )
1053                * pct
1054                for strategy_col in df_strategy_columns
1055            ]
1056        if "benchmark" in df:
1057            bench_vol = (
1058                _stats.volatility(
1059                    df["benchmark"], win_year, True, prepare_returns=False
1060                )
1061                * pct
1062            )
1063
1064            vol_ = [ret_vol, bench_vol]
1065            if isinstance(ret_vol, list):
1066                metrics["Volatility (ann.) %"] = list(_pd.core.common.flatten(vol_))
1067            else:
1068                metrics["Volatility (ann.) %"] = vol_
1069
1070            if isinstance(returns, _pd.Series):
1071                metrics["R^2"] = _stats.r_squared(
1072                    df["returns"], df["benchmark"], prepare_returns=False
1073                )
1074                metrics["Information Ratio"] = _stats.information_ratio(
1075                    df["returns"], df["benchmark"], prepare_returns=False
1076                )
1077            elif isinstance(returns, _pd.DataFrame):
1078                metrics["R^2"] = (
1079                    [
1080                        _stats.r_squared(
1081                            df[strategy_col], df["benchmark"], prepare_returns=False
1082                        ).round(2)
1083                        for strategy_col in df_strategy_columns
1084                    ]
1085                ) + ["-"]
1086                metrics["Information Ratio"] = (
1087                    [
1088                        _stats.information_ratio(
1089                            df[strategy_col], df["benchmark"], prepare_returns=False
1090                        ).round(2)
1091                        for strategy_col in df_strategy_columns
1092                    ]
1093                ) + ["-"]
1094        else:
1095            if isinstance(returns, _pd.Series):
1096                metrics["Volatility (ann.) %"] = [ret_vol]
1097            elif isinstance(returns, _pd.DataFrame):
1098                metrics["Volatility (ann.) %"] = ret_vol
1099
1100        metrics["Calmar"] = _stats.calmar(df, prepare_returns=False, periods=win_year)
1101        metrics["Skew"] = _stats.skew(df, prepare_returns=False)
1102        metrics["Kurtosis"] = _stats.kurtosis(df, prepare_returns=False)
1103
1104        metrics["~~~~~~~~~~"] = blank
1105
1106        metrics["Expected Daily %%"] = (
1107            _stats.expected_return(df, compounded=compounded, prepare_returns=False)
1108            * pct
1109        )
1110        metrics["Expected Monthly %%"] = (
1111            _stats.expected_return(
1112                df, compounded=compounded, aggregate="ME", prepare_returns=False
1113            )
1114            * pct
1115        )
1116        metrics["Expected Yearly %%"] = (
1117            _stats.expected_return(
1118                df, compounded=compounded, aggregate="YE", prepare_returns=False
1119            )
1120            * pct
1121        )
1122
1123        metrics["Daily Value-at-Risk %"] = -abs(
1124            _stats.var(df, prepare_returns=False) * pct
1125        )
1126        metrics["Expected Shortfall (cVaR) %"] = -abs(
1127            _stats.cvar(df, prepare_returns=False) * pct
1128        )
1129
1130    # returns
1131    metrics["~~"] = blank
1132    comp_func = _stats.comp if compounded else lambda x: _np.sum(x, axis=0)
1133
1134    today = df.index[-1]  # _dt.today()
1135    metrics["MTD %"] = comp_func(df[df.index >= _dt(today.year, today.month, 1)]) * pct
1136
1137    d = today - relativedelta(months=3)
1138    metrics["3M %"] = comp_func(df[df.index >= d]) * pct
1139
1140    d = today - relativedelta(months=6)
1141    metrics["6M %"] = comp_func(df[df.index >= d]) * pct
1142
1143    metrics["YTD %"] = comp_func(df[df.index >= _dt(today.year, 1, 1)]) * pct
1144
1145    d = today - relativedelta(years=1)
1146    metrics["1Y %"] = comp_func(df[df.index >= d]) * pct
1147
1148    d = today - relativedelta(months=35)
1149    metrics["3Y (ann.) %"] = (
1150        _stats.cagr(df[df.index >= d], 0.0, compounded, win_year) * pct
1151    )
1152
1153    d = today - relativedelta(months=59)
1154    metrics["5Y (ann.) %"] = (
1155        _stats.cagr(df[df.index >= d], 0.0, compounded, win_year) * pct
1156    )
1157
1158    d = today - relativedelta(years=10)
1159    metrics["10Y (ann.) %"] = (
1160        _stats.cagr(df[df.index >= d], 0.0, compounded, win_year) * pct
1161    )
1162
1163    metrics["All-time (ann.) %"] = _stats.cagr(df, 0.0, compounded, win_year) * pct
1164
1165    # best/worst
1166    if mode.lower() == "full":
1167        metrics["~~~"] = blank
1168        metrics["Best Day %"] = (
1169            _stats.best(df, compounded=compounded, prepare_returns=False) * pct
1170        )
1171        metrics["Worst Day %"] = _stats.worst(df, prepare_returns=False) * pct
1172        metrics["Best Month %"] = (
1173            _stats.best(
1174                df, compounded=compounded, aggregate="ME", prepare_returns=False
1175            )
1176            * pct
1177        )
1178        metrics["Worst Month %"] = (
1179            _stats.worst(df, aggregate="ME", prepare_returns=False) * pct
1180        )
1181        metrics["Best Year %"] = (
1182            _stats.best(
1183                df, compounded=compounded, aggregate="YE", prepare_returns=False
1184            )
1185            * pct
1186        )
1187        metrics["Worst Year %"] = (
1188            _stats.worst(
1189                df, compounded=compounded, aggregate="YE", prepare_returns=False
1190            )
1191            * pct
1192        )
1193
1194    # dd
1195    metrics["~~~~"] = blank
1196    for ix, row in dd.iterrows():
1197        metrics[ix] = row
1198    metrics["Recovery Factor"] = _stats.recovery_factor(df)
1199    metrics["Ulcer Index"] = _stats.ulcer_index(df)
1200    metrics["Serenity Index"] = _stats.serenity_index(df, rf)
1201
1202    # win rate
1203    if mode.lower() == "full":
1204        metrics["~~~~~"] = blank
1205        metrics["Avg. Up Month %"] = (
1206            _stats.avg_win(
1207                df, compounded=compounded, aggregate="ME", prepare_returns=False
1208            )
1209            * pct
1210        )
1211        metrics["Avg. Down Month %"] = (
1212            _stats.avg_loss(
1213                df, compounded=compounded, aggregate="ME", prepare_returns=False
1214            )
1215            * pct
1216        )
1217
1218        # Get the win rate
1219        win_rate = _stats.win_rate(df, prepare_returns=False)
1220
1221        # Number of win days in total
1222        metrics["Win Days"] = win_rate * len(df)
1223
1224        # Number of loss days in total
1225        metrics["Loss Days"] = len(df) - metrics["Win Days"]
1226
1227        metrics["Win Days %%"] = win_rate * pct
1228        metrics["Win Month %%"] = (
1229            _stats.win_rate(
1230                df, compounded=compounded, aggregate="ME", prepare_returns=False
1231            )
1232            * pct
1233        )
1234        metrics["Win Quarter %%"] = (
1235            _stats.win_rate(
1236                df, compounded=compounded, aggregate="QE", prepare_returns=False
1237            )
1238            * pct
1239        )
1240        metrics["Win Year %%"] = (
1241            _stats.win_rate(
1242                df, compounded=compounded, aggregate="YE", prepare_returns=False
1243            )
1244            * pct
1245        )
1246
1247        if "benchmark" in df:
1248            metrics["~~~~~~~~~~~~"] = blank
1249            if isinstance(returns, _pd.Series):
1250                greeks = _stats.greeks(
1251                    df["returns"], df["benchmark"], win_year, prepare_returns=False
1252                )
1253                metrics["Beta"] = [str(round(greeks["beta"], 2)), "-"]
1254                metrics["Alpha"] = [str(round(greeks["alpha"], 2)), "-"]
1255                metrics["Correlation"] = [
1256                    str(round(df["benchmark"].corr(df["returns"]) * pct, 2)) + "%",
1257                    "-",
1258                ]
1259                metrics["Treynor Ratio"] = [
1260                    str(
1261                        round(
1262                            _stats.treynor_ratio(
1263                                df["returns"], df["benchmark"], win_year, rf
1264                            )
1265                            * pct,
1266                            2,
1267                        )
1268                    )
1269                    + "%",
1270                    "-",
1271                ]
1272            elif isinstance(returns, _pd.DataFrame):
1273                greeks = [
1274                    _stats.greeks(
1275                        df[strategy_col],
1276                        df["benchmark"],
1277                        win_year,
1278                        prepare_returns=False,
1279                    )
1280                    for strategy_col in df_strategy_columns
1281                ]
1282                metrics["Beta"] = [str(round(g["beta"], 2)) for g in greeks] + ["-"]
1283                metrics["Alpha"] = [str(round(g["alpha"], 2)) for g in greeks] + ["-"]
1284                metrics["Correlation"] = (
1285                    [
1286                        str(round(df["benchmark"].corr(df[strategy_col]) * pct, 2))
1287                        + "%"
1288                        for strategy_col in strategy_colname
1289                    ]
1290                ) + ["-"]
1291                metrics["Treynor Ratio"] = (
1292                    [
1293                        str(
1294                            round(
1295                                _stats.treynor_ratio(
1296                                    df[strategy_col], df["benchmark"], win_year, rf
1297                                )
1298                                * pct,
1299                                2,
1300                            )
1301                        )
1302                        + "%"
1303                        for strategy_col in strategy_colname
1304                    ]
1305                ) + ["-"]
1306
1307    # prepare for display
1308    for col in metrics.columns:
1309        try:
1310            metrics[col] = metrics[col].astype(float).round(2)
1311            if display or "internal" in kwargs:
1312                metrics[col] = metrics[col].astype(str)
1313        except Exception:
1314            pass
1315        if (display or "internal" in kwargs) and "*int" in col:
1316            metrics[col] = metrics[col].str.replace(".0", "", regex=False)
1317            metrics.rename({col: col.replace("*int", "")}, axis=1, inplace=True)
1318        if (display or "internal" in kwargs) and "%" in col:
1319            metrics[col] = metrics[col] + "%"
1320
1321    try:
1322        metrics["Longest DD Days"] = _pd.to_numeric(metrics["Longest DD Days"]).astype(
1323            "int"
1324        )
1325        metrics["Avg. Drawdown Days"] = _pd.to_numeric(
1326            metrics["Avg. Drawdown Days"]
1327        ).astype("int")
1328
1329        if display or "internal" in kwargs:
1330            metrics["Longest DD Days"] = metrics["Longest DD Days"].astype(str)
1331            metrics["Avg. Drawdown Days"] = metrics["Avg. Drawdown Days"].astype(str)
1332    except Exception:
1333        metrics["Longest DD Days"] = "-"
1334        metrics["Avg. Drawdown Days"] = "-"
1335        if display or "internal" in kwargs:
1336            metrics["Longest DD Days"] = "-"
1337            metrics["Avg. Drawdown Days"] = "-"
1338
1339    metrics.columns = [col if "~" not in col else "" for col in metrics.columns]
1340    metrics = metrics.T
1341
1342    if "benchmark" in df:
1343        column_names = [strategy_colname, benchmark_colname]
1344        if isinstance(strategy_colname, list):
1345            metrics.columns = list(_pd.core.common.flatten(column_names))
1346        else:
1347            metrics.columns = column_names
1348    else:
1349        if isinstance(strategy_colname, list):
1350            metrics.columns = strategy_colname
1351        else:
1352            metrics.columns = [strategy_colname]
1353
1354    # cleanups
1355    metrics.replace([-0, "-0"], 0, inplace=True)
1356    metrics.replace(
1357        [
1358            _np.nan,
1359            -_np.nan,
1360            _np.inf,
1361            -_np.inf,
1362            "-nan%",
1363            "nan%",
1364            "-nan",
1365            "nan",
1366            "-inf%",
1367            "inf%",
1368            "-inf",
1369            "inf",
1370        ],
1371        "-",
1372        inplace=True,
1373    )
1374
1375    # move benchmark to be the first column always if present
1376    if "benchmark" in df:
1377        metrics = metrics[
1378            [benchmark_colname]
1379            + [col for col in metrics.columns if col != benchmark_colname]
1380        ]
1381
1382    if display:
1383        print(_tabulate(metrics, headers="keys", tablefmt="simple"))
1384        return None
1385
1386    if not sep:
1387        metrics = metrics[metrics.index != ""]
1388
1389    # remove spaces from column names
1390    metrics = metrics.T
1391    metrics.columns = [
1392        c.replace(" %", "").replace(" *int", "").strip() for c in metrics.columns
1393    ]
1394    metrics = metrics.T
1395
1396    return metrics

calculates and displays various performance metrics

def plots( returns, benchmark=None, grayscale=False, figsize=(8, 5), mode='basic', compounded=True, periods_per_year=365, prepare_returns=True, match_dates=True, **kwargs):
1399def plots(
1400    returns,
1401    benchmark=None,
1402    grayscale=False,
1403    figsize=(8, 5),
1404    mode="basic",
1405    compounded=True,
1406    periods_per_year=365,
1407    prepare_returns=True,
1408    match_dates=True,
1409    **kwargs,
1410):
1411    """Plots for strategy performance"""
1412
1413    benchmark_colname = kwargs.get("benchmark_title", "Benchmark")
1414    strategy_colname = kwargs.get("strategy_title", "Strategy")
1415    active = kwargs.get("active", "False")
1416
1417    if (
1418        isinstance(returns, _pd.DataFrame)
1419        and len(returns.columns) > 1
1420        and isinstance(strategy_colname, str)
1421    ):
1422        strategy_colname = list(returns.columns)
1423
1424    win_year, win_half_year = _get_trading_periods(periods_per_year)
1425
1426    if match_dates is True:
1427        returns = returns.dropna()
1428
1429    if prepare_returns:
1430        returns = _utils._prepare_returns(returns)
1431
1432    if isinstance(returns, _pd.Series):
1433        returns.name = strategy_colname
1434    elif isinstance(returns, _pd.DataFrame):
1435        returns.columns = strategy_colname
1436
1437    if mode.lower() != "full":
1438        _plots.snapshot(
1439            returns,
1440            grayscale=grayscale,
1441            figsize=(figsize[0], figsize[0]),
1442            show=True,
1443            mode=("comp" if compounded else "sum"),
1444            benchmark_title=benchmark_colname,
1445            strategy_title=strategy_colname,
1446        )
1447
1448        if isinstance(returns, _pd.Series):
1449            _plots.monthly_heatmap(
1450                returns,
1451                benchmark,
1452                grayscale=grayscale,
1453                figsize=(figsize[0], figsize[0] * 0.5),
1454                show=True,
1455                ylabel=False,
1456                compounded=compounded,
1457                active=active,
1458            )
1459        elif isinstance(returns, _pd.DataFrame):
1460            for col in returns.columns:
1461                _plots.monthly_heatmap(
1462                    returns[col].dropna(),
1463                    benchmark,
1464                    grayscale=grayscale,
1465                    figsize=(figsize[0], figsize[0] * 0.5),
1466                    show=True,
1467                    ylabel=False,
1468                    returns_label=col,
1469                    compounded=compounded,
1470                    active=active,
1471                )
1472
1473        return
1474
1475    returns = _pd.DataFrame(returns)
1476
1477    # prepare timeseries
1478    if benchmark is not None:
1479        benchmark = _utils._prepare_benchmark(benchmark, returns.index)
1480        benchmark.name = benchmark_colname
1481        if match_dates is True:
1482            returns, benchmark = _match_dates(returns, benchmark)
1483
1484    _plots.returns(
1485        returns,
1486        benchmark,
1487        grayscale=grayscale,
1488        figsize=(figsize[0], figsize[0] * 0.6),
1489        show=True,
1490        ylabel=False,
1491        prepare_returns=False,
1492    )
1493
1494    _plots.log_returns(
1495        returns,
1496        benchmark,
1497        grayscale=grayscale,
1498        figsize=(figsize[0], figsize[0] * 0.5),
1499        show=True,
1500        ylabel=False,
1501        prepare_returns=False,
1502    )
1503
1504    if benchmark is not None:
1505        _plots.returns(
1506            returns,
1507            benchmark,
1508            match_volatility=True,
1509            grayscale=grayscale,
1510            figsize=(figsize[0], figsize[0] * 0.5),
1511            show=True,
1512            ylabel=False,
1513            prepare_returns=False,
1514        )
1515
1516    _plots.yearly_returns(
1517        returns,
1518        benchmark,
1519        grayscale=grayscale,
1520        figsize=(figsize[0], figsize[0] * 0.5),
1521        show=True,
1522        ylabel=False,
1523        prepare_returns=False,
1524    )
1525
1526    _plots.histogram(
1527        returns,
1528        benchmark,
1529        grayscale=grayscale,
1530        figsize=(figsize[0], figsize[0] * 0.5),
1531        show=True,
1532        ylabel=False,
1533        prepare_returns=False,
1534    )
1535
1536    small_fig_size = (figsize[0], figsize[0] * 0.35)
1537    if len(returns.columns) > 1:
1538        small_fig_size = (
1539            figsize[0],
1540            figsize[0] * (0.33 * (len(returns.columns) * 0.66)),
1541        )
1542
1543    _plots.daily_returns(
1544        returns,
1545        benchmark,
1546        grayscale=grayscale,
1547        figsize=small_fig_size,
1548        show=True,
1549        ylabel=False,
1550        prepare_returns=False,
1551        active=active,
1552    )
1553
1554    if benchmark is not None:
1555        _plots.rolling_beta(
1556            returns,
1557            benchmark,
1558            grayscale=grayscale,
1559            window1=win_half_year,
1560            window2=win_year,
1561            figsize=small_fig_size,
1562            show=True,
1563            ylabel=False,
1564            prepare_returns=False,
1565        )
1566
1567    _plots.rolling_volatility(
1568        returns,
1569        benchmark,
1570        grayscale=grayscale,
1571        figsize=small_fig_size,
1572        show=True,
1573        ylabel=False,
1574        period=win_half_year,
1575    )
1576
1577    _plots.rolling_sharpe(
1578        returns,
1579        grayscale=grayscale,
1580        figsize=small_fig_size,
1581        show=True,
1582        ylabel=False,
1583        period=win_half_year,
1584    )
1585
1586    _plots.rolling_sortino(
1587        returns,
1588        grayscale=grayscale,
1589        figsize=small_fig_size,
1590        show=True,
1591        ylabel=False,
1592        period=win_half_year,
1593    )
1594
1595    if isinstance(returns, _pd.Series):
1596        _plots.drawdowns_periods(
1597            returns,
1598            grayscale=grayscale,
1599            figsize=(figsize[0], figsize[0] * 0.5),
1600            show=True,
1601            ylabel=False,
1602            prepare_returns=False,
1603        )
1604    elif isinstance(returns, _pd.DataFrame):
1605        for col in returns.columns:
1606            _plots.drawdowns_periods(
1607                returns[col],
1608                grayscale=grayscale,
1609                figsize=(figsize[0], figsize[0] * 0.5),
1610                show=True,
1611                ylabel=False,
1612                title=col,
1613                prepare_returns=False,
1614            )
1615
1616    _plots.drawdown(
1617        returns,
1618        grayscale=grayscale,
1619        figsize=(figsize[0], figsize[0] * 0.4),
1620        show=True,
1621        ylabel=False,
1622    )
1623
1624    if isinstance(returns, _pd.Series):
1625        _plots.monthly_heatmap(
1626            returns,
1627            benchmark,
1628            grayscale=grayscale,
1629            figsize=(figsize[0], figsize[0] * 0.5),
1630            returns_label=returns.name,
1631            show=True,
1632            ylabel=False,
1633            active=active,
1634        )
1635    elif isinstance(returns, _pd.DataFrame):
1636        for col in returns.columns:
1637            _plots.monthly_heatmap(
1638                returns[col],
1639                benchmark,
1640                grayscale=grayscale,
1641                figsize=(figsize[0], figsize[0] * 0.5),
1642                show=True,
1643                ylabel=False,
1644                returns_label=col,
1645                compounded=compounded,
1646                active=active,
1647            )
1648
1649    if isinstance(returns, _pd.Series):
1650        _plots.distribution(
1651            returns,
1652            grayscale=grayscale,
1653            figsize=(figsize[0], figsize[0] * 0.5),
1654            show=True,
1655            title=returns.name,
1656            ylabel=False,
1657            prepare_returns=False,
1658        )
1659    elif isinstance(returns, _pd.DataFrame):
1660        for col in returns.columns:
1661            _plots.distribution(
1662                returns[col],
1663                grayscale=grayscale,
1664                figsize=(figsize[0], figsize[0] * 0.5),
1665                show=True,
1666                title=col,
1667                ylabel=False,
1668                prepare_returns=False,
1669            )

Plots for strategy performance