Features

Interactive charts

Plot backtests, candlesticks, indicators, signals, and parameter heatmaps with Plotly

How do you see what a strategy actually did? VBT plots backtests, prices, indicators, signals, and parameter results as interactive Plotly charts in Python: zoom into a single trade, hover for exact values, and combine any of them in one figure.

Plot a backtest with orders, trade P&L, a custom RSI panel, and drawdowns
data = vbt.YFData.pull("BTC-USD", start="2023-01-01", end="2024-01-01")
rsi = data.run("rsi", window=14).rsi
pf = vbt.PF.from_signals(data, rsi.vbt.crossed_below(30), rsi.vbt.crossed_above(70))

def plot_rsi(pf, rsi, add_trace_kwargs=None, fig=None):  
    rsi.rename("RSI").vbt.plot(add_trace_kwargs=add_trace_kwargs, fig=fig)

pf.plot(
    subplots=[
        "orders",
        "trade_pnl",
        ("rsi", dict(title="RSI", plot_func=plot_rsi, rsi=rsi)),
        "drawdowns",
    ],
    settings=dict(bm_returns=False),
).show()
Bitcoin RSI strategy chart for 2023 with buy and sell orders, trade profit and loss, the RSI indicator, and drawdowns Figure data (JSON)

Portfolio and trade charts

pf.plot() draws a dashboard from named subplots. Built-in subplots include orders, trades, trade_pnl, trade_signals, cumulative_returns, drawdowns, underwater, gross_exposure, allocations, cash, assets, and value, and each can be configured through subplot_settings. Every subplot also exists as its own method, such as pf.plot_cumulative_returns(), pf.plot_underwater(), or pf.plot_allocations() for multi-asset weights over time.

Records plot themselves too: pf.trades.plot() shows each trade from entry to exit with its profit, and pf.drawdowns.plot(top_n=5) shades the largest drawdowns. For returns, a calendar heatmap of monthly returns is one line: pf.returns_acc.resample("M").ts_heatmap().

Price, indicator, and signal charts

Data objects plot as candlesticks or OHLC bars with volume. Indicators and signals are added to the same figure by passing fig:

Overlay moving averages and crossover signals on candlesticks
sub = data.loc["2023-04-01":"2023-08-31"]
fast = sub.close.rolling(10).mean()
slow = sub.close.rolling(30).mean()
entries = fast.vbt.crossed_above(slow)
exits = fast.vbt.crossed_below(slow)

fig = sub.plot(plot_volume=False)
fast.rename("SMA 10").vbt.plot(fig=fig)
slow.rename("SMA 30").vbt.plot(fig=fig)
entries.vbt.signals.plot_as_entries(fast, fig=fig)  
exits.vbt.signals.plot_as_exits(fast, fig=fig)
fig.show()
Bitcoin candlestick chart from April to August 2023 with 10 and 30 day moving averages and crossover entry and exit markers Figure data (JSON)

Indicators built with VBT have a plot method with sensible defaults, and TA-Lib indicators plot according to their output types, for example as lines or histograms.

Heatmaps and other generic plots

Any Series or DataFrame gets plotting methods through its accessor: line, scatter, bar, histogram, box, heatmap, and 3D volume plots. A parameter grid with three parameters becomes a heatmap with a slider for the third:

Explore three RSI strategy parameters with a heatmap and a slider
btc = vbt.YFData.pull("BTC-USD", start="2020-01-01", end="2025-01-01")
rsi = btc.run("rsi", window=14).rsi

@vbt.parameterized(merge_func="concat")  
def sharpe(lower, upper, sl_stop):
    pf = vbt.PF.from_signals(
        btc,
        rsi.vbt.crossed_below(lower),
        rsi.vbt.crossed_above(upper),
        sl_stop=sl_stop,
    )
    return pf.sharpe_ratio

sharpes = sharpe(
    vbt.Param([20, 25, 30, 35, 40]),
    vbt.Param([60, 65, 70, 75, 80]),
    vbt.Param([0.05, 0.1, 0.2]),
)
sharpes.vbt.heatmap(x_level="lower", y_level="upper", slider_level="sl_stop").show()
Heatmap of Sharpe ratios for RSI entry and exit thresholds with a slider for the stop loss level Figure data (JSON)

When Plotly Express has a chart you need, call it from the accessor, for example df.vbt.px.parallel_coordinates(). Cross-validation splits have their own plots that show the training and test periods of every split.

Customization

Every plotting method takes trace_kwargs for the trace, add_trace_kwargs to place it in a subplot, and layout keywords for the figure. For example, slow.vbt.plot(trace_kwargs=dict(line=dict(color="orange", dash="dash")), fig=fig) draws one dashed orange line. vbt.make_subplots creates figures with several panels, and since the result is a regular Plotly figure, everything Plotly offers still works afterwards.

Themes and defaults live in vbt.settings.plotting, for example vbt.settings.set_theme("dark") for dark charts. For markets that close overnight or on weekends, fig.auto_rangebreaks(freq="1D") adds Plotly range breaks for the missing time, so candles sit next to each other instead of leaving gaps.

Charts that update with new data

You can update the data in existing Plotly traces while keeping the figure's layout and styling. Connect those updates to a DataUpdater to refresh a candlestick chart as new bars arrive, or update lines for prices and indicators in a notebook widget. This is useful for watching the latest part of a series without creating a new chart each time.

The Data pipelines page covers scheduled data updates, and Live simulation covers continuing a portfolio on new bars. You can also save successive chart images as frames in a GIF with imageio to replay how a chart developed.

Large data and output

A minute chart over several years has millions of points, which a browser draws slowly. With plotly-resampler installed, set vbt.settings.plotting["use_resampler"] = True and charts load a downsampled view that refines as you zoom. Alternatively, select a shorter range before plotting, or resample the portfolio first: pf.resample("1D") returns a daily portfolio with the same total return, and its charts draw a point per day.

Share interactive charts and export images

Figures show interactively in notebooks and browsers with fig.show(), as static images with fig.show_png() or fig.show_svg(), and save to files with fig.write_image() or fig.write_html(). Static images are rendered by Plotly's kaleido package, so install it first, and update it if an export hangs. The renderer can be set per figure or globally.

An HTML export keeps zoom, hover labels, and legend controls, so someone else can explore the results in a browser without installing Python or VBT. Plotly includes the chart data and its JavaScript library in the file by default. See Plotly's HTML export guide for saving a figure or combining several charts in one page. Use PNG or SVG when you need a fixed image for a report or presentation.

Copyright © 2021–2026 Oleg Polakow. All rights reserved.

Site content and documentation are provided for using and evaluating VectorBT PRO and for educational purposes. Any other use, including building or supporting competing products or services, requires prior written consent.