Nautilus Visualization
The Nautilus
[visualization]extras package is a Plotly-based interactive tearsheet system (NOT Bokeh) that renders self-contained HTML reports from backtest results. Install viauv pip install "nautilus_trader[visualization]"(depends onplotly>=6.3.1). The system has three parts: a chart registry with eight built-in charts (run_info, stats_table, equity, drawdown, monthly_returns, distribution, rolling_sharpe, yearly_returns, bars_with_fills), a theme system (four built-ins plus custom registration), andTearsheetConfigdeclarative composition. Output is a single HTML file per backtest, embeddable anywhere a browser opens. It is backtest-only - there is no live dashboard surface in this extras package. For Cortana MK3 this is one of three parallel visualization surfaces: (1) preserved MK2 operator dashboard repointed at Nautilus cache for live ops, (2) Nautilus tearsheets for backtest research artifacts, (3) ML Dashboard #81 for per-tenant ML transparency. The three are complementary, not competing.
This page specializes Nautilus Concepts at the visualization layer, runs parallel to Nautilus Portfolio and Nautilus Reports (which are the programmatic analysis surfaces - this page is the rendered surface that consumes their output), and depends on Nautilus Cache only indirectly (the dashboard repoint reads cache, not tearsheets).
Why this page exists
The MK3 roadmap commits to preserving the existing MK2 operator dashboard
(HTML/CSS/JS) by repointing its data layer at the Nautilus cache. Separately,
it commits to using nautilus_trader[visualization] for backtest tearsheets.
Separately again, it preserves ML Dashboard #81 as a per-tenant ML
transparency view. Three surfaces. This page documents the middle one -
what the extras package actually provides - so the question “do we still
need to hand-build the equity-curve / return-distribution panels for ML
Dashboard #81?” has a sourced answer.
Core claim
“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.”
Plotly. Not Bokeh. HTML output. Backtest-focused. Extensible through a
chart registry. This is the entire scope of the [visualization] extras -
nothing more, nothing less.
The extras package - what’s in the box
Install
uv pip install "nautilus_trader[visualization]"
# or, if you only want the underlying lib:
uv pip install "plotly>=6.3.1"The extras pull plotly>=6.3.1 as the only added dependency. There is no
Bokeh, no Dash, no Streamlit in the package. The doc is explicit:
“The visualization system requires plotly>=6.3.1.”
Three structural parts
- Chart Registry - Decoupled chart definitions. Built-in charts plus
user-registered custom charts. Registry pattern means custom charts can
coexist with built-ins under the same
TearsheetConfig.chartslist. - Theme System - Consistent styling primitives (colors, fonts,
backgrounds). Four built-in themes plus
register_theme(...)for custom. - Configuration -
TearsheetConfigis the declarative top-level: which charts, which theme, which layout, what title, what height, benchmark overlay yes/no.
Output format
“All visualization outputs are self-contained HTML files that can be viewed in any modern browser, shared with stakeholders, or archived for future reference.”
One HTML file. Self-contained (data inline). Open in any browser. No server.
PNG export is not surfaced as a first-class flag in create_tearsheet(...),
though Plotly’s underlying fig.write_image(...) can be called against the
returned figure object if needed (Plotly supports PNG/SVG/PDF export with
the kaleido extra).
Tearsheet - the primary artifact
A tearsheet in Nautilus parlance is a single HTML page that combines
multiple charts and statistics into one interactive visualization. It is
generated after a backtest run, by passing the engine to
create_tearsheet(...).
Quick start
from nautilus_trader.analysis import create_tearsheet
from nautilus_trader.backtest.engine 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 eight default charts, the light theme, and an automatic 4×2 grid layout. Open the HTML in a browser.
Customized
from nautilus_trader.analysis import (
TearsheetConfig,
TearsheetDrawdownChart,
TearsheetEquityChart,
TearsheetRunInfoChart,
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, narrow stats to one currency:
from nautilus_trader.model.currencies import USD
create_tearsheet(
engine=engine,
output_path="usd_only.html",
currency=USD, # None (default) shows all currencies separately
)Plot taxonomy - the eight built-in charts
| Chart name | Type | Description |
|---|---|---|
run_info | Table | Run metadata: ID, start/end, iterations, event/order/position counts, account starting/ending balances per currency. |
stats_table | Table | Performance metrics in three sections: PnL Statistics (per currency), Returns Statistics (Sharpe, Sortino, max drawdown), General Statistics (total trades, avg duration). |
equity | Line | Cumulative returns over backtest period. Optional benchmark overlay. |
drawdown | Area | Drawdown percentage from peak equity. |
monthly_returns | Heatmap | Monthly 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. |
Default grid: when all eight ship at once, a 4×2 grid is auto-laid out
with row heights [0.50, 0.22, 0.16, 0.12] - top row gets the most space,
which is where run_info and stats_table live by default.
Equity curve with benchmark
import pandas as pd
# Index should be datetime, aligned with strategy returns
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. Index alignment is the caller’s responsibility - Nautilus does no resampling.
Theme system
Four built-ins:
| Theme name | Description | Use case |
|---|---|---|
plotly_white | Clean light theme with dark gray headers. | Default, professional reports. |
plotly_dark | Dark background, standard Plotly colors. | Low-light environments. |
nautilus | Light with NautilusTrader brand colors. | Official light. |
nautilus_dark | Dark with teal/cyan signature. | Official dark. |
Selecting a theme:
config = TearsheetConfig(theme="nautilus_dark")Custom theme registration:
from nautilus_trader.analysis import register_theme
register_theme(
name="cortana",
template="plotly_dark",
colors={
"primary": "#00ff9c", # cortana terminal green
"positive": "#2ecc71",
"negative": "#e74c3c",
"neutral": "#95a5a6",
"background":"#0a0a0a",
"grid": "#1a1a1a",
# Optional table colors (defaults provided if omitted)
"table_section": "#1a1a1a",
"table_row_odd": "#0a0a0a",
"table_row_even": "#111111",
"table_text": "#00ff9c",
},
)
config = TearsheetConfig(theme="cortana")Backward compatibility: if table_* colors are omitted, sensible defaults
are computed from background and grid, so legacy themes still work.
Configuration - TearsheetConfig parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
charts | list[TearsheetChart] | All built-ins | Chart objects, in order. |
theme | str | "plotly_white" | Theme name. |
layout | GridLayout | None (auto) | Custom subplot grid. |
title | str | Auto-generated | Tearsheet title. |
include_benchmark | bool | True | Show benchmark when provided. |
benchmark_name | str | "Benchmark" | Display name. |
height | int | 1500 | Total pixel height. |
show_logo | bool | True | Display NautilusTrader logo (reserved for future use). |
Custom layout example:
from nautilus_trader.analysis import GridLayout
config = TearsheetConfig(
charts=[...],
layout=GridLayout(
rows=2,
cols=2,
heights=[0.60, 0.40],
vertical_spacing=0.08,
horizontal_spacing=0.12,
),
)When layout=None, grid dimensions and row heights auto-compute from chart
count. For non-default chart sets, expect to set layout explicitly to
avoid weird auto-layout behavior.
Customization - adding custom charts
Two registration tiers. The registry pattern is the same shape as Nautilus’s
other extension points (similar to register_statistic on
PortfolioAnalyzer).
Tier 1: standalone chart (stable API)
from nautilus_trader.analysis.tearsheet import register_chart
import plotly.graph_objects as go
def my_custom_chart(returns, output_path=None, title="Custom", theme="plotly_white"):
from nautilus_trader.analysis.themes import get_theme
theme_config = get_theme(theme)
fig = go.Figure()
fig.add_trace(go.Scatter(
x=returns.index,
y=returns.cumsum(),
mode="lines",
line={"color": theme_config["colors"]["primary"]},
))
fig.update_layout(template=theme_config["template"], title=title)
if output_path:
fig.write_html(output_path)
return fig
register_chart("my_custom", my_custom_chart)This is the public API. Charts registered this way are accessible via
get_chart() / list_charts() and can be invoked standalone.
Tier 2: tearsheet integration (internal API - use with care)
from nautilus_trader.analysis.tearsheet import _register_tearsheet_chart
def _render_my_metric(fig, row, col, returns, theme_config, **kwargs):
metric = returns.rolling(30).std() * 100
fig.add_trace(
go.Scatter(x=returns.index, y=metric, mode="lines"),
row=row, col=col,
)
_register_tearsheet_chart(
name="volatility",
subplot_type="scatter",
title="Rolling Volatility (30-day)",
renderer=_render_my_metric,
)
# Now usable in TearsheetConfig.charts
config = TearsheetConfig(
charts=[
TearsheetStatsTableChart(),
TearsheetEquityChart(),
TearsheetCustomChart(chart="volatility"),
],
)The doc is explicit about the stability boundary:
“The
_register_tearsheet_chartfunction is internal API and may change between releases. For most use cases, preferregister_chartfor standalone charts or contribute new built-in charts upstream.”
For Cortana: anything we add for ourselves should use register_chart.
Anything that touches the tearsheet grid layout is internal API and must
be pinned to a specific Nautilus version.
Custom statistics
Custom charts pair naturally with custom PortfolioStatistic registrations
(see Nautilus Portfolio):
from nautilus_trader.analysis.statistic import PortfolioStatistic
class MyCustomStatistic(PortfolioStatistic):
def calculate_from_returns(self, returns):
return ...
analyzer.register_statistic(MyCustomStatistic())
# Now appears in stats_returns for any custom chart that consumes it.Output formats
| Format | How |
|---|---|
| HTML (interactive, self-contained) | Default. create_tearsheet(engine=..., output_path="x.html"). |
| HTML in Jupyter | The returned figure renders inline in notebooks (Plotly’s standard _repr_html_). |
| PNG / SVG / PDF | Not first-class. Call fig.write_image(...) on the returned figure (requires kaleido). |
| JSON | Plotly figures serialize to JSON via fig.to_json() if you need a programmatic surface. |
The “HTML files contain all data inline and can be several megabytes for long backtests” caveat is real. For multi-month / multi-year SPY 0DTE replays, expect 5-50 MB per tearsheet. Sharing via Slack/email is fine; storing 1000 of them per tenant is wasteful. Generate per-run, archive selectively.
Offline analysis - precomputed stats path
For analyzing results without a BacktestEngine instance:
from nautilus_trader.analysis.tearsheet import create_tearsheet_from_stats
import pandas as pd
stats_pnls = {"USD": {"PnL (total)": 1500.0, "Win Rate": 0.55, ...}}
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
create_tearsheet_from_stats(
stats_pnls=stats_pnls,
stats_returns=stats_returns,
stats_general=stats_general,
returns=returns,
output_path="offline.html",
)The dictionary keys must match PortfolioAnalyzer.get_performance_stats_*()
output (see Nautilus Portfolio). This is the
right path for:
- Comparing N stored backtests side-by-side.
- Integrating with external pipelines (write stats to a DB, render later).
- A/B comparing strategy variants without re-running each engine.
Cortana MK3 implications
The MK3 roadmap defines three visualization surfaces. The extras package covers exactly one of them.
Surface 1 - preserved MK2 operator dashboard (live ops)
Source: Nautilus cache via Python API (read-only).
Use case: today’s trades, open positions, current signal scores, score
breakdowns, in-flight order state.
Plotly tearsheets do NOT serve this surface. Tearsheets are post-run
artifacts; the operator dashboard is live. The repoint is straightforward
in shape (self.cache.positions_open(), self.cache.orders_open(),
self.cache.quote_tick(instrument_id), etc. - see
Nautilus Cache) but is not what
nautilus_trader[visualization] provides. The dashboard preservation is
correct architectural framing; the extras package is irrelevant to it.
Surface 2 - backtest tearsheets (research artifacts)
Source: nautilus_trader[visualization] extras after each
engine.run().
Use case: equity curves, sharpe, drawdown, fill quality, return
distributions across replay periods.
This is exactly what the extras package provides out of the box. Six
of the eight built-in charts (equity, drawdown, monthly_returns,
distribution, rolling_sharpe, yearly_returns) cover the core research
questions. bars_with_fills covers fill-quality visualization. stats_table
covers Brier/AUC-class metrics if we register custom PortfolioStatistics
for them. The MK3 roadmap’s “Phase 1 expectancy harness becomes Nautilus
tearsheet customization in M2” is achievable without writing chart code.
Surface 3 - ML Dashboard #81 (per-tenant ML transparency)
Source: Nautilus event stream + ML model artifacts. Use case: kNN neighbors per signal, feature health, calibration curves, per-tenant ML transparency. Hand-built. Not served by extras. ML Dashboard #81 is interactive, live, per-tenant, and queries against the meta-model - it’s a different beast than a static post-backtest HTML.
The actual question - does extras-cover-equity-and-distribution mean we don’t need them in #81?
No, but the answer is more nuanced than “no.”
ML Dashboard #81 wants equity / drawdown / return-distribution panels embedded in a live, per-tenant, ML-aware UI. Nautilus extras give us those panels as standalone HTML tearsheets, not as embeddable React/JS components. Three implications:
- For research / postmortem use: extras is sufficient. We open the HTML. Done. Don’t rebuild.
- For live operator viewing inside ML Dashboard #81: extras is not sufficient as-is. The HTML output is monolithic; the ML Dashboard wants per-position drill-downs, per-feature live updates, per-tenant scoping.
- The pragmatic middle path: ML Dashboard #81 can embed
Plotly-figure JSON (via
fig.to_json()on the same Plotly figures the tearsheet code produces), then re-render client-side usingplotly.js. That re-uses the chart logic without rebuilding it - but it requires ML Dashboard’s frontend to shipplotly.js(~3 MB) which is a real bundle-size question.
Recommended posture: rely on extras for surface #2 (backtest
research). Treat ML Dashboard #81 surface #3 as separate hand-built work,
but lift the chart logic (returns calculation, drawdown calculation,
rolling-sharpe calculation) from PortfolioAnalyzer rather than
re-implementing it. Do not assume the extras HTML drops into ML
Dashboard #81 unchanged.
Is the dashboard repoint at Nautilus cache straightforward?
Yes, with two caveats.
The shape is straightforward - every panel in the MK2 dashboard can be
expressed as a Nautilus cache query. self.cache.positions_open(),
self.cache.orders_open(), self.cache.quote_tick(instrument_id),
self.cache.account(account_id). See Nautilus Cache
for the full read surface. The cache is exactly the right shape for what
the dashboard needs.
Caveat 1 - process boundary. The dashboard is a separate Node/Python
process today, querying a SQLite view. The Nautilus cache lives inside the
TradingNode process. Two paths: (a) embed dashboard server inside the
trading-node process (couples them - bad); (b) externalize cache via Redis
(DatabaseConfig) and have dashboard process read Redis directly (good,
already the recommended pattern for live deployments per
Nautilus Cache). The roadmap’s “Dockerized IB Gateway
adopted (per task #96)” decision aligns naturally with externalized cache.
Caveat 2 - the bridge. During M1 the spike plan calls out an intermediate “local dashboard sidecar that mirrors Nautilus events to SQLite for the existing MK2 dashboard to read (Option B from spike planning - temporary until M2 reuses the dashboard cleanly).” That’s the right shape. The full repoint at Redis-backed cache lands in M3 with the production cutover.
So: straightforward in principle, with operational caveats around process boundaries and the M1→M3 bridge.
Best practices
From the doc, with Cortana-specific gloss:
Chart selection
- Default for exploratory analysis - cheap, all metrics visible.
- Customize when ready - drop charts that don’t matter for the strategy.
- Remove irrelevant charts - reduces visual clutter and HTML file size.
For SPY 0DTE specifically:
monthly_returnsheatmap is uninteresting (we only trade weekdays during market hours, no monthly seasonality signal at this granularity);yearly_returnsis uninteresting until we have multi-year data.
Theme usage
plotly_whitefor professional reports.nautilus_darkfor low-light viewing.- Custom Cortana theme for branded outputs (terminal green on black, matching the existing dashboard).
Performance
- Tearsheets contain all data inline; can be several MB per run.
- For long replays, generate separate tearsheets for different timeframes (e.g., one per week of replay, one per month).
- For very large datasets, use individual chart functions standalone rather than full tearsheets.
Custom statistics integration
“Custom charts work best when paired with custom statistics registered in the
PortfolioAnalyzer. This ensures your visualizations display metrics computed consistently with the rest of the system.”
For Cortana: register Brier-score, AUC, hit-rate-at-confidence-threshold
as PortfolioStatistics (see Nautilus Portfolio),
THEN render them via the extras tearsheet. Do not compute them in chart
code - keeps the metric implementation in one place.
Anti-patterns
- Treating extras tearsheets as a live dashboard surface. They’re not. They’re post-run artifacts. Live ops belongs to the preserved MK2 dashboard reading the cache.
- Embedding the entire HTML tearsheet in ML Dashboard #81 via iframe. Works mechanically, terrible UX, no per-tenant scoping, no live updates. Lift the figure JSON instead.
- Forking the chart code into ML Dashboard #81. Don’t reimplement
drawdown/rolling-sharpe - use
PortfolioAnalyzeras the single source of truth for return metrics, then render client-side. - Pinning to
_register_tearsheet_chart. Internal API. Pinned Nautilus version required if used. Preferregister_chartstandalone. - Assuming PNG/SVG export is first-class. It isn’t.
kaleidois required forfig.write_image(...). If we want PDF reports of monthly research, installkaleidoseparately. - Ignoring index alignment for benchmark comparisons. Nautilus does no resampling. Garbage in, garbage on the chart.
Open questions for the 2026-05-09 spike
- Plotly version pin. Extras requires
plotly>=6.3.1. Does this conflict with anything we currently ship? (Checkpyproject.toml.) - Tearsheet HTML size on a 4-week SPY 0DTE replay. Concrete data point needed to decide whether to archive every run or just key ones.
- Custom theme parity with existing dashboard. Can we hit the exact terminal-green look, or do we accept “close enough” and reserve pixel-perfect for the live dashboard?
- Embedding Plotly figure JSON in ML Dashboard #81. Bundle-size
question: can ML Dashboard frontend afford
plotly.js? - Multi-tenant tearsheet generation. Per-tenant per-run output paths
(
tenants/{id}/runs/{ts}/tearsheet.html)? Where do they live in M5 web app? Probably S3/CDN, not embedded - but this is M5 territory.
See Also
- Nautilus Concepts - full concept canon parent
- Nautilus Portfolio - parallel; the
PortfolioAnalyzerproduces the stats that tearsheets render - Nautilus Reports - parallel; programmatic reporting surface; tearsheets are the rendered counterpart
- Nautilus Cache - what the live dashboard surface reads (different from tearsheets)
- Nautilus Tutorials - backtest tutorials that produce the engines tearsheets render from
- Spike plan - Step 7 dashboard repoint feasibility:
~/conductor/workspaces/cortanaroi-mk2/belo-horizonte/plans/2026-05-09-nautilus-spike.md - MK3 roadmap - visualization stack table near the bottom:
~/conductor/workspaces/cortanaroi-mk2/belo-horizonte/plans/2026-05-09-mk3-roadmap.md - ML Dashboard #81 - per-tenant ML transparency view (separate surface)
Timeline
2026-05-07 | Cody - Filed during pre-spike concept mastery sweep batch 3.