Source code for macrosynergy.pnl.pnl_table

"""
Presentation-ready HTML rendering of ``NaivePnL.evaluate_pnls()`` statistics
as a grouped, ledger-style table for reports and notebooks.
"""

from typing import Dict, List, Optional, Sequence, Tuple, Union

import pandas as pd


[docs]class HTMLTable: """ The HTML table returned by ``NaivePnL.evaluate_pnls_html()``. Displays as a formatted table when returned as the last line of a notebook cell. The raw HTML is available as ``.data``, e.g. to save it to a file with ``Path("table.html").write_text(table.data)``. """ def __init__(self, data: str): self.data = data def _repr_html_(self) -> str: return self.data
DEFAULT_PNL_METRIC_GROUPS: Dict[str, List[str]] = { "Performance": ["Return %", "Sharpe Ratio", "Sortino Ratio"], "Risk & drawdown": [ "St. Dev. %", "Peak to Trough Draw %", "Max Draw Recovery (months)", "Benchmark correlation", ], "Robustness": [ "Sharpe Stability Ratio", "Prob. Sharpe Ratio > 0.25", "Prob. Sharpe Ratio > 0.5", "Prob. Sharpe Ratio > 0.75", "Top 5% Monthly PnL Share", "Traded Months", ], } # INK matches the macrosynergy package's default LinePlot/view_timeseries # color: Plotter.__init__ calls sns.set_theme(style="darkgrid", # palette="colorblind"), and seaborn's "colorblind" palette's first swatch is # #0173b2 - the blue every unstyled macrosynergy time series chart uses. _INK, _INK2, _MUTED = "#0174b2c6", "#52514e", "#898781" _BAND, _ZEBRA = "#f4f3ef", "#faf9f6" _NUM_FIELD_W = "7ch" # 4ch integer slot + 3ch fraction slot # matplotlib never sets a custom typeface in this package's plotting code # (only ever *sizes*, never *family*), so every chart inherits matplotlib's # bundled default, DejaVu Sans. Matched here, with a system-font fallback for # renderers (e.g. browsers) that don't have DejaVu Sans registered. _TEXT_FONT = "'DejaVu Sans', system-ui, -apple-system, 'Segoe UI', sans-serif" _MONO_FONT = ( "'DejaVu Sans Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" ) # Every visual rule lives here, addressed by class, so a caller-supplied # `custom_css` block - injected in a second <style> tag right after this one # - can override any of it: same selector, later in source order, wins. _DEFAULT_CSS = f""" .pnl-wrap {{ font-family:{_TEXT_FONT}; width:fit-content; min-width:640px; max-width:100%; background:#fff; border:1px solid {_INK}; border-radius:2px; overflow:hidden; }} .pnl-title {{ padding:16px 20px 2px 20px; font-size:16px; font-weight:700; color:{_INK}; }} .pnl-subtitle {{ padding:0 20px 12px 20px; font-size:12px; color:{_MUTED}; }} .pnl-table {{ border-collapse:collapse; width:100%; border-top:2px solid {_INK}; }} .pnl-thead-row {{ background:{_BAND}; }} .pnl-th-corner {{ text-align:left; padding:8px 20px; font-size:11.5px; letter-spacing:.04em; text-transform:uppercase; color:{_MUTED}; border-bottom:1px solid {_INK}; white-space:nowrap; }} .pnl-th {{ text-align:center; padding:8px 20px; font-size:11.5px; letter-spacing:.04em; text-transform:uppercase; color:{_INK}; font-weight:700; border-bottom:1px solid {_INK}; white-space:nowrap; }} .pnl-group-cell {{ padding:9px 20px; font-size:11px; letter-spacing:.08em; text-transform:uppercase; color:#fff; font-weight:700; background:{_INK}; text-align:left; }} .pnl-row-even {{ background:{_ZEBRA}; }} .pnl-row-odd {{ background:#ffffff; }} .pnl-label {{ padding:8px 20px; text-align:left; font-size:13.5px; color:{_INK2}; white-space:nowrap; }} .pnl-value {{ padding:8px 20px; text-align:center; font-size:14.5px; font-family:{_MONO_FONT}; font-variant-numeric:tabular-nums; font-weight:400; white-space:nowrap; }} .pnl-value-strategy {{ color:{_INK2}; }} .pnl-value-bench {{ color:{_MUTED}; }} .pnl-num {{ display:inline-block; width:{_NUM_FIELD_W}; text-align:right; }} .pnl-num-split {{ display:inline-grid; grid-template-columns:4ch 3ch; width:{_NUM_FIELD_W}; text-align:left; }} .pnl-num-int {{ text-align:right; }} .pnl-num-frac {{ text-align:left; }} .pnl-footnote {{ padding:8px 20px 14px 20px; font-size:11px; color:{_MUTED}; }} """.strip() def _fmt(metric: str, v: float, whole_number_metrics: Sequence[str]) -> str: if v != v: # NaN return "" return f"{v:,.0f}" if metric in whole_number_metrics else f"{v:,.2f}" def _decimal_aligned(s: str) -> str: # Decimal values: split "int.frac" so the "." lands in a fixed column # across every row, the way a LaTeX `&`-aligned tabular column would. # Whole numbers (no ".") have no fraction slot to sit in, so instead # they're right-aligned across the *whole* field - landing their last # digit on the same column as the last fractional digit of the decimal # rows, rather than at the (nonexistent) decimal point. int_part, dot, frac_part = s.partition(".") if not dot: return f'<span class="pnl-num">{int_part}</span>' return ( f'<span class="pnl-num-split">' f'<span class="pnl-num-int">{int_part}</span>' f'<span class="pnl-num-frac">.{frac_part}</span>' f"</span>" )
[docs]def pnl_table_html( tbr: pd.DataFrame, groups: List[Tuple[str, List[str]]], headlines: List[str], bench: Union[str, List[str]], headline_labels: List[str], bench_label: Union[str, List[str]], whole_number_metrics: Sequence[str] = (), plus_prefix: Dict[str, Sequence[str]] = None, negative_share_row: Optional[str] = None, footnotes: bool = True, title: str = "Naive PnL statistics", subtitle: str = "", custom_css: Optional[str] = None, ) -> str: """Render a grouped, ledger-style HTML table from an already-computed ``evaluate_pnls()`` statistics DataFrame. All headline (strategy) columns share one style; all benchmark columns are shown in a distinct, muted style. Parameters ---------- tbr : pd.DataFrame Output of ``evaluate_pnls()`` (or a renamed copy of it), indexed by metric name, columns are PnL categories. groups : list of (section title, [metric names]) Row layout and order. Every metric referenced must be a row in ``tbr``. headlines : list of str ``tbr`` columns to show as the (non-benchmark) strategy columns, in display order. bench : str or list of str ``tbr`` column(s) to show as the benchmark (muted) column(s), in display order. headline_labels, bench_label : list of str, str or list of str Column header labels, matching ``headlines`` 1:1, and ``bench`` 1:1 (a bare str is only valid when ``bench`` is a single column). whole_number_metrics : sequence of str, default () Metric names formatted with no decimal places (e.g. "Traded Months") instead of two. plus_prefix : dict of {row name: [column names]}, optional Cells to prefix with "+" (e.g. an unrecovered drawdown still in progress). A footnote explaining the "+" is appended below the table whenever this marks at least one cell (subject to ``footnotes``). negative_share_row : str, optional Row name to check for negative values (e.g. "Top 5% Monthly PnL Share", which reads negative whenever total PnL is negative, since it's top-5%-months PnL divided by total PnL). A footnote explaining this is appended below the table whenever that row has a negative cell (subject to ``footnotes``). footnotes : bool, default True Whether to render the explanatory footnotes above (for "+" and/or ``negative_share_row``) at all. The underlying "+" prefix on cells is unaffected - this only toggles the footnote text. title, subtitle : str Header text shown above the table. custom_css : str, optional Raw CSS injected in a second ``<style>`` block, right after the table's own default stylesheet - same-specificity selectors win by source order, so rules targeting the classes below override the defaults without needing ``!important``: - ``.pnl-wrap`` - outer container (font, border, background). Sizes to fit its content (``width:fit-content``) between a 640px minimum and its embedding page's width as a maximum, so extra columns or long labels widen the box rather than being clipped or forcing a scrollbar - keeping the whole table selectable in one paste, e.g. into Word. - ``.pnl-title``, ``.pnl-subtitle`` - header text above the table - ``.pnl-table`` - the ``<table>`` element itself - ``.pnl-thead-row``, ``.pnl-th-corner``, ``.pnl-th`` - header row background and column-header cells - ``.pnl-group-cell`` - section title bars (e.g. "Risk & drawdown") - ``.pnl-row-even``, ``.pnl-row-odd`` - zebra-striped data rows - ``.pnl-label`` - the metric-name cell of a data row - ``.pnl-value``, ``.pnl-value-strategy``, ``.pnl-value-bench`` - numeric cells; the latter two color strategy vs. benchmark columns differently - ``.pnl-num``, ``.pnl-num-split``, ``.pnl-num-int``, ``.pnl-num-frac`` - decimal-point alignment of the numbers themselves - ``.pnl-footnote`` - the "+"/negative-share explanation footnotes, if shown Returns ------- str A self-contained HTML string. """ benches = bench if isinstance(bench, list) else [bench] bench_labels = bench_label if isinstance(bench_label, list) else [bench_label] cols = [*headlines, *benches] col_classes = ["pnl-value-strategy"] * len(headlines) + ["pnl-value-bench"] * len( benches ) plus_prefix = plus_prefix or {} used_plus = False used_negative_share = False def num_cell(metric, col, value_class): nonlocal used_plus, used_negative_share v = tbr.loc[metric, col] if metric == negative_share_row and v == v and v < 0: used_negative_share = True fmt = _fmt(metric, v, whole_number_metrics) if fmt and col in plus_prefix.get(metric, ()): used_plus = True fmt = "+" + fmt aligned = _decimal_aligned(fmt) return f'<td class="pnl-value {value_class}">{aligned}</td>' rows = [] row_i = 0 n_cols = len(cols) + 1 for group, metrics in groups: rows.append( f'<tr><td colspan="{n_cols}" class="pnl-group-cell">{group}</td></tr>' ) for m in metrics: row_class = "pnl-row-even" if row_i % 2 == 0 else "pnl-row-odd" cells = "".join( num_cell(m, col, value_class) for col, value_class in zip(cols, col_classes) ) rows.append( f'<tr class="{row_class}"><td class="pnl-label">{m}</td>{cells}</tr>' ) row_i += 1 header_labels = [*headline_labels, *bench_labels] header_cells = "".join(f'<th class="pnl-th">{lbl}</th>' for lbl in header_labels) footnote_lines = [] if footnotes and used_plus: footnote_lines.append( "+ drawdown had not yet recovered as of the last observation" ) if footnotes and used_negative_share: footnote_lines.append( f"{negative_share_row} is top-5%-months PnL / total PnL, so it " "reads negative when total PnL itself is negative" ) footnote = ( f'<div class="pnl-footnote">{"<br>".join(footnote_lines)}</div>' if footnote_lines else "" ) custom_style = f"<style>{custom_css}</style>" if custom_css else "" return f"""<style>{_DEFAULT_CSS}</style> {custom_style} <div class="pnl-wrap"> <div class="pnl-title">{title}</div> <div class="pnl-subtitle">{subtitle}</div> <table class="pnl-table"> <thead><tr class="pnl-thead-row"> <th class="pnl-th-corner"></th> {header_cells} </tr></thead> <tbody>{"".join(rows)}</tbody> </table> {footnote} </div>"""