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
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
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
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
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
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
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