Skip to content
All library documents

Building Backtest Tear Sheets with Performance Charts and Return Conventions

Article NautilusTrader

Summary

This guide describes NautilusTrader’s system for turning completed backtests into interactive or static performance reports. Users can select charts and themes, include run metadata and performance statistics, and inspect equity, drawdown, monthly and yearly returns, rolling Sharpe, return distributions, or price bars annotated with fills. It also explains when a retained backtest node is needed, how currency filtering affects statistics and return charts, and how benchmarks can be overlaid on an equity curve.

A central analytical choice is whether monthly and yearly returns compound over changing portfolio value or use fixed initial capital. Compounded returns reflect time-weighted growth; simple returns can suit constant-capital strategies, provided position sizes remain fixed in quantity. The guide is implementation documentation, not evidence about any strategy’s results. It cautions that benchmark dates should align with strategy returns and that mixed-currency return charts require a currency selection. Tear sheets summarize backtest outputs; they do not validate assumptions or guarantee live performance.

Key ideas

  • A tear sheet combines selected performance charts and statistics into a browser-viewable report or static export.
  • Chart configuration controls which views appear, while themes control their styling.
  • Currency selection matters for return charts built from mixed-currency backtests.
  • Monthly and yearly returns can be compounded or measured against fixed starting capital.
  • Fixed-capital return figures align with constant-quantity sizing more closely than equity-based sizing.

Tags

Full text
# Visualization


# Visualization

NautilusTrader provides interactive HTML tearsheets for analyzing backtest results through
an extensible visualization system built on Plotly. You can generate reports with minimal
code and add custom charts and themes.

## Overview

The visualization system has three parts:

1. **Chart Registry** - Decoupled chart definitions that can be extended with custom visualizations.
2. **Theme System** - Consistent styling with built-in and custom themes.
3. **Configuration** - Declarative specification of what to render and how to display it.

Tearsheets are written as **self-contained HTML files** that can be viewed in any modern
browser, shared with stakeholders, or archived for future reference. Passing a static
image extension (such as `.png` or `.pdf`) as the output path exports a static image
via Kaleido instead.

:::note
The visualization system requires the `visualization` extra. It installs Pandas for
DataFrame handling, Plotly for interactive figures, and Kaleido for static image export:

```bash
uv pip install --pre "nautilus_trader[visualization]"
```

:::

## Tearsheets

A tearsheet is a performance report that combines multiple charts and
statistics into a single interactive visualization. Tearsheets are generated after
completing a backtest run and provide immediate visual feedback on strategy performance.

### Quick start

Generate a tearsheet with default settings:

```python
from nautilus_trader.analysis import create_tearsheet
from nautilus_trader.backtest import BacktestEngine

# After running your backtest
engine.run()

# Generate tearsheet
create_tearsheet(
    engine=engine,
    output_path="backtest_results.html",
)
```

This produces an HTML file with all default charts, using the light theme and automatic
layout. Open `backtest_results.html` in your browser to view the interactive tearsheet.

### Backtest result input

Let `result` be a `BacktestResult` returned by a completed backtest. Pass it without its node for a
result-only tearsheet:

```python
create_tearsheet(
    engine=result,
    output_path="backtest_results.html",
)
```

To include starting account balances from node reports, retain the node state. The node is also
required when the configured tearsheet includes a cache-backed chart such as `bars_with_fills`.

Follow the complete [`BacktestNode` setup](backtesting/apis-and-runs.md#high-level-api),
setting `dispose_on_completion=False` on its `BacktestRunConfig`. Then pass the completed result and
retained node:

```python
create_tearsheet(
    engine=result,
    node=node,
    output_path="backtest_results.html",
)
```

:::warning
Passing a node whose matching run configuration enables disposal raises `ValueError` because its
cache and reports are no longer available.
:::

### Customization

Control which charts appear and how they're styled:

```python
from nautilus_trader.config import TearsheetConfig
from nautilus_trader.analysis import TearsheetDrawdownChart
from nautilus_trader.analysis import TearsheetEquityChart
from nautilus_trader.analysis import TearsheetRunInfoChart
from nautilus_trader.analysis import TearsheetStatsTableChart

config = TearsheetConfig(
    charts=[
        TearsheetRunInfoChart(),
        TearsheetStatsTableChart(),
        TearsheetEquityChart(),
        TearsheetDrawdownChart(),
    ],
    theme="nautilus_dark",
    height=2000,
)

create_tearsheet(
    engine=engine,
    output_path="custom_tearsheet.html",
    config=config,
)
```

### Currency filtering

For multi-currency backtests, filter statistics to a specific currency:

```python
from nautilus_trader.model import Currency

create_tearsheet(
    engine=engine,
    output_path="usd_only.html",
    currency=Currency.from_str("USD"),  # Shows only USD statistics
)
```

When `currency` is `None` (default), statistics for all currencies are displayed
separately in the tearsheet. For `BacktestEngine` input, return-based charts require a
single currency: they are derived from portfolio equity snapshots, falling back to
account reports, and cannot be built for mixed-currency accounts without a filter;
pass `currency` for multi-currency backtests so return charts use the selected
currency.

For `BacktestResult` input, `currency` filters PnL statistics and account balances. The result's
**stored return series remains unchanged**.

## Available charts

The tearsheet can include any combination of the following built-in charts:

| Chart Name        | Type        | Description                                             |
| ----------------- | ----------- | ------------------------------------------------------- |
| `run_info`        | Table       | Run metadata and account balances.                      |
| `stats_table`     | Table       | Performance statistics (PnL, returns, general metrics). |
| `equity`          | Line        | Cumulative returns over time with optional benchmark.   |
| `drawdown`        | Area        | Drawdown percentage from peak equity.                   |
| `monthly_returns` | Heatmap     | Monthly portfolio return percentages organized by year. |
| `distribution`    | Histogram   | Distribution of individual return values.               |
| `rolling_sharpe`  | Line        | 60-day rolling Sharpe ratio.                            |
| `yearly_returns`  | Bar         | Annual return percentages.                              |
| `bars_with_fills` | Candlestick | Price bars (OHLC) with order fills overlaid as markers. |

All charts are registered in the chart registry and are configured via chart objects in
`TearsheetConfig.charts` (each chart object maps to a built-in chart name).

### Run information table

The `run_info` chart displays key metadata about the backtest run:

- Run ID, start time, finish time
- Backtest period (start/end dates)
- Total iterations processed
- Event, order, and position counts
- Account starting and ending balances (per currency)

This table appears in the top-left position by default.

### Performance statistics table

The `stats_table` chart displays performance metrics organized into sections:

- **PnL Statistics** (per currency): Total PnL, win rate, profit factor, etc.
- **Returns Statistics**: Sharpe ratio, Sortino ratio, max drawdown, etc.
- **General Statistics**: Total trades, average trade duration, etc.

This table appears in the top-right position by default.

### Equity curve

The `equity` chart plots cumulative returns over the backtest period. When `benchmark_returns`
is provided to `create_tearsheet()`, the benchmark is overlaid for comparison.

```python
import pandas as pd

# Load benchmark returns (e.g., from a market index)
# Index should be datetime, aligned with strategy returns timeframe
benchmark_returns = pd.read_csv("sp500_returns.csv", index_col=0, parse_dates=True)["return"]

create_tearsheet(
    engine=engine,
    output_path="with_benchmark.html",
    benchmark_returns=benchmark_returns,
    benchmark_name="S&P 500",
)
```

The benchmark series is plotted as-is; ensure the index aligns with your strategy's
return dates for accurate comparison.

### Monthly and yearly returns

The `monthly_returns` and `yearly_returns` charts default to compounded (time-weighted)
returns: each cell measures the period's gain against the running start-of-period balance,
and the periods compound to the total return.

Set `compounding=False` to report simple, non-compounding returns measured against fixed
initial capital. Each cell then measures the period's gain as a percentage of the starting
capital, so the periods sum to the total return instead of compounding to it. This is the
nominal rate of return, the convention used for constant-capital strategies that trade fixed
size and withdraw profits.

```python
config = TearsheetConfig(
    charts=[
        TearsheetMonthlyReturnsChart(compounding=False),
        TearsheetYearlyReturnsChart(compounding=False),
    ],
)
create_tearsheet(engine=engine, config=config)
```

The standalone `create_monthly_returns_heatmap()` and `create_yearly_returns()` functions
accept the same `compounding` argument. For the non-compounding figures to faithfully
represent constant capital, size positions at a fixed quantity rather than as a fraction of
current equity; otherwise later periods inflate as the running balance grows.

## Themes

Themes control the visual styling of charts including colors, fonts, and backgrounds.
NautilusTrader provides four built-in themes:

| Theme Name      | Description                                   | Use Case                       |
| --------------- | --------------------------------------------- | ------------------------------ |
| `plotly_white`  | Clean light theme with dark gray headers.     | Default, professional reports. |
| `plotly_dark`   | Dark background with standard Plotly colors.  | Low-light environments.        |
| `nautilus`      | Light theme with NautilusTrader brand colors. | Official light mode.           |
| `nautilus_dark` | Dark theme with teal/cyan signature colors.   | Official dark mode.            |

### Selecting a theme

Specify the theme in `TearsheetConfig`:

```python
config = TearsheetConfig(theme="nautilus_dark")
create_tearsheet(engine=engine, config=config)
```

### Custom themes

Register a custom theme for consistent branding across all visualizations:

```python
from nautilus_trader.analysis import register_theme

register_theme(
    name="corporate",
    template="plotly_white",  # Base Plotly template
    colors={
        "primary": "#003366",  # Navy blue
        "positive": "#2e8b57",  # Sea green
        "negative": "#c41e3a",  # Cardinal red
        "neutral": "#808080",  # Gray
        "background": "#ffffff",  # White
        "grid": "#e5e5e5",  # Light gray
        # Optional table colors (defaults will be provided if omitted)
        "table_section": "#e5e5e5",
        "table_row_odd": "#f8f8f8",
        "table_row_even": "#ffffff",
        "table_text": "#000000",
    },
)

# Use the custom theme
config = TearsheetConfig(theme="corporate")
```

The theme system automatically provides sensible defaults for `table_*` colors based on
the `background` and `grid` colors, ensuring backward compatibility with themes registered
before table-specific colors were introduced.

## Configuration

The `TearsheetConfig` class provides declarative control over tearsheet generation:

```python
from nautilus_trader.analysis import GridLayout
from nautilus_trader.config import TearsheetConfig
from nautilus_trader.analysis import TearsheetDrawdownChart
from nautilus_trader.analysis import TearsheetEquityChart
from nautilus_trader.analysis import TearsheetStatsTableChart

config = TearsheetConfig(
    charts=[
        TearsheetEquityChart(),
        TearsheetDrawdownChart(),
        TearsheetStatsTableChart(),
    ],
    theme="nautilus_dark",
    title="Q4 2024 Strategy Performance",
    height=1800,
    include_benchmark=True,
    benchmark_name="SPY",
    layout=GridLayout(
        rows=2,
        cols=2,
        heights=[0.60, 0.40],
        vertical_spacing=0.08,
        horizontal_spacing=0.12,
    ),
)
```

### Configuration parameters

| Parameter           | Type                   | Default          | Description                         |
| ------------------- | ---------------------- | ---------------- | ----------------------------------- |
| `charts`            | `list[TearsheetChart]` | Built-ins        | Charts to include, in order.        |
| `theme`             | `str`                  | `"plotly_white"` | Theme name for styling.             |
| `layout`            | `GridLayout`           | `None`           | Custom subplot grid layout.         |
| `title`             | `str`                  | Auto-generated   | Tearsheet title.                    |
| `include_benchmark` | `bool`                 | `True`           | Show benchmark when provided.       |
| `benchmark_name`    | `str`                  | `"Benchmark"`    | Display name for benchmark.         |
| `height`            | `int`                  | `1500`           | Total height in pixels.             |
| `show_logo`         | `bool`                 | `True`           | Reserved for future logo rendering. |

When `layout` is `None`, the grid dimensions and row heights are automatically calculated
based on the number of charts. For 8 charts (the default), a 4x2 grid is used with
heights `[0.50, 0.22, 0.16, 0.12]` to give more space to the top row tables.

## Custom charts

The registry pattern lets you add custom charts. Charts are functions that
render traces onto a Plotly figure object.

### Registering a custom chart

```python
from nautilus_trader.analysis import register_chart
import plotly.graph_objects as go


def my_custom_chart(returns, output_path=None, title="Custom Chart", theme="plotly_white"):
    """
    Create a custom visualization.

    This function signature matches the built-in chart functions for consistency.
    """
    from nautilus_trader.analysis import get_theme

    theme_config = get_theme(theme)

    # Create your visualization
    fig = go.Figure()
    fig.add_trace(
        go.Scatter(
            x=returns.index,
            y=returns.cumsum(),
            mode="lines",
            name="Custom Metric",
            line={"color": theme_config["colors"]["primary"]},
        )
    )

    fig.update_layout(
        title=title,
        template=theme_config["template"],
        xaxis_title="Date",
        yaxis_title="Value",
    )

    if output_path:
        fig.write_html(output_path)

    return fig


# Register the chart for standalone use (via `get_chart()` / `list_charts()`)
register_chart("my_custom", my_custom_chart)
```

### Tearsheet integration

For tearsheet integration with proper grid placement, use `register_tearsheet_chart`. Unlike
`register_chart` (which registers a standalone function that returns its own figure), a tearsheet
renderer draws traces directly onto a shared subplot grid cell, so its signature takes the target
`fig` plus the `row` and `col` to render into.

```python
from nautilus_trader.config import TearsheetConfig
from nautilus_trader.analysis import TearsheetCustomChart
from nautilus_trader.analysis import TearsheetEquityChart
from nautilus_trader.analysis import TearsheetStatsTableChart
from nautilus_trader.analysis import register_tearsheet_chart


def _render_my_metric(fig, row, col, returns, theme_config, **kwargs):
    """
    Render custom metric directly onto a subplot.

    Parameters
    ----------
    fig : go.Figure
        The figure to add traces to.
    row : int
        Subplot row position.
    col : int
        Subplot column position.
    returns : pd.Series
        Strategy returns series supplied to the renderer.
    theme_config : dict
        Theme configuration dictionary.
    **kwargs : dict
        Additional parameters (stats_pnls, stats_returns, benchmark_returns, etc.).
    """
    metric_values = returns.rolling(30).std() * 100  # Example metric

    fig.add_trace(
        go.Scatter(
            x=returns.index,
            y=metric_values,
            mode="lines",
            name="30-Day Volatility",
            line={"color": theme_config["colors"]["neutral"]},
        ),
        row=row,
        col=col,
    )

    fig.update_xaxes(title_text="Date", row=row, col=col)
    fig.update_yaxes(title_text="Volatility (%)", row=row, col=col)


# Register for tearsheet use
register_tearsheet_chart(
    name="volatility",
    subplot_type="scatter",
    title="Rolling Volatility (30-day)",
    renderer=_render_my_metric,
)

# Now "volatility" can be used in TearsheetConfig.charts:
config = TearsheetConfig(
    charts=[
        TearsheetStatsTableChart(),
        TearsheetEquityChart(),
        TearsheetCustomChart(chart="volatility"),
    ],
)
```

The renderer function receives all necessary data (returns, statistics, theme configuration)
and renders directly onto the specified subplot position.

## Offline analysis

For situations where you have precomputed statistics but not a `BacktestEngine` instance,
use the lower-level API:

```python
import pandas as pd

from nautilus_trader.analysis import create_tearsheet_from_stats

# Load precomputed data. The structure matches BacktestResult stats fields.
stats_pnls = {"USD": {"PnL (total)": 1500.0, "Win Rate": 0.55, ...}}  # Per-currency
stats_returns = {"Sharpe Ratio (252 days)": 1.2, "Max Drawdown": -0.15, ...}
stats_general = {"Avg Winner": 100.0, "Avg Loser": -50.0, ...}
returns = pd.Series(...)  # Daily returns with datetime index

create_tearsheet_from_stats(
    stats_pnls=stats_pnls,
    stats_returns=stats_returns,
    stats_general=stats_general,
    returns=returns,
    output_path="offline_analysis.html",
)
```

The dictionary keys should match those returned by `engine.get_result().stats_pnls`,
`engine.get_result().stats_returns`, and `engine.get_result().stats_general`.

This approach is useful for:

- Analyzing results from multiple backtest runs stored separately.
- Comparing strategies using precomputed metrics.
- Integrating with external analysis pipelines.

## Best practices

### Chart selection

- Use default charts for exploratory analysis to see all available metrics.
- Customize charts when you know which metrics matter for your strategy.
- Remove irrelevant charts to reduce visual clutter and file size.

### Theme usage

- Use `plotly_white` for professional reports and presentations.
- Use `nautilus_dark` for official materials or low-light viewing.
- Create custom themes to match internal guidelines or personal preferences.

### Performance considerations

- Tearsheet HTML files contain all data inline and can be several megabytes for long backtests.
- Consider generating separate tearsheets for different analysis timeframes.
- For very large datasets, use the individual chart functions instead of full tearsheets.

### Custom statistics integration

Custom charts work best when paired with statistics supplied through the same
`stats_pnls`, `stats_returns`, and `stats_general` dictionaries used by the built-in
tearsheet charts. For `BacktestEngine` input these values come from
`engine.get_result()`, so a statistic registered with `Portfolio.register_statistic()` reaches the
tearsheet without any extra wiring; see
[Custom statistics](portfolio.md#custom-statistics). For offline analysis, pass compatible
dictionaries directly to `create_tearsheet_from_stats()`:

```python
stats_returns = {
    "Sharpe Ratio (252 days)": 1.2,
    "Custom Volatility Score": 0.42,
}
```

## API levels

The visualization system provides two API levels, plus standalone chart functions:

### High-level API

Recommended for most use cases:

```python
create_tearsheet(engine=engine, config=config)
```

Automatically extracts data from a `BacktestEngine` or `BacktestResult`, generates all configured
charts, and produces a complete HTML tearsheet.

### Low-level API

For advanced customization or offline analysis:

```python
create_tearsheet_from_stats(
    stats_pnls=stats_pnls,
    stats_returns=stats_returns,
    stats_general=stats_general,
    returns=returns,
    run_info=run_info,
    account_info=account_info,
    config=config,
)
```

Provides fine-grained control over data inputs and allows analysis of precomputed statistics.

### Standalone chart functions

Individual chart functions can be used independently to generate single-purpose HTML visualizations
or Plotly figures for custom analysis workflows.

#### Price bars with fills

The `create_bars_with_fills` function generates a candlestick chart with order fills overlaid,
useful for visually analyzing strategy execution within price action. It can be used standalone
or included in tearsheets:

```python
from nautilus_trader.analysis import create_bars_with_fills
from nautilus_trader.analysis import create_tearsheet
from nautilus_trader.analysis import TearsheetBarsWithFillsChart
from nautilus_trader.config import TearsheetConfig
from nautilus_trader.analysis import TearsheetEquityChart
from nautilus_trader.analysis import TearsheetStatsTableChart
from nautilus_trader.model import BarType

# Standalone usage
bar_type = BarType.from_str("ESM4.XCME-1-MINUTE-LAST-EXTERNAL")
fig = create_bars_with_fills(
    engine=engine,
    bar_type=bar_type,
    title="ES Futures - Entry/Exit Analysis",
)
fig.show()  # Display in Jupyter
fig.write_html("bars_with_fills.html")  # Or save to file

# Include in tearsheet
config = TearsheetConfig(
    charts=[
        TearsheetStatsTableChart(),
        TearsheetEquityChart(),
        TearsheetBarsWithFillsChart(
            bar_type="ESM4.XCME-1-MINUTE-LAST-EXTERNAL",
            title="Bars with Fills",
        ),
    ],
)
create_tearsheet(engine=engine, config=config)

# Multiple bars-with-fills charts in one tearsheet
config = TearsheetConfig(
    charts=[
        TearsheetStatsTableChart(),
        TearsheetEquityChart(),
        TearsheetBarsWithFillsChart(
            bar_type=f"{instrument.id}-5-MINUTE-MID-INTERNAL",
            title=f"Bars with Order Fills - {instrument.id}",
        ),
        TearsheetBarsWithFillsChart(
            bar_type=f"{other_instrument.id}-5-MINUTE-MID-INTERNAL",
            title=f"Bars with Order Fills - {other_instrument.id}",
        ),
    ],
)
create_tearsheet(engine=engine, config=config)
```

The visualization shows candlesticks for OHLC price action with triangle markers representing order
fills (up-triangles for buys and down-triangles for sells, colored with the theme's positive and
negative colors). Charts that need extra configuration (like `bar_type`) take those parameters
directly on the chart object (e.g. `TearsheetBarsWithFillsChart(bar_type=...)`).

Other individual chart functions include `create_equity_curve`, `create_drawdown_chart`,
`create_monthly_returns_heatmap`, and more. See the API reference for the complete list.

## Related guides

- [Backtesting](backtesting/) - Learn how to run backtests that generate tearsheets.
- [Reports](reports.md) - Understand the underlying statistics displayed in tearsheets.
- [Portfolio](portfolio.md) - Explore portfolio tracking and performance metrics.

Shown in full with attribution under the source's licence. Licence: LGPL-3.0

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.