Python13 min read·

vectorbt Tutorial 2026: Fast Backtesting in Python

Learn vectorbt - run a moving-average backtest in five lines, sweep thousands of parameter combinations in seconds, and avoid lookahead bias.

Testing every moving-average crossover with windows between 2 and 100 days means 4,851 distinct parameter combinations. In an event-driven backtester that loops through bars in Python, a sweep like that over a decade of daily data is an overnight job. In vectorbt, on a laptop, it takes a few seconds.

That performance gap is the whole reason the library exists. vectorbt, created by Oleg Polakow and first released as open source in 2019, reframes backtesting as an array problem: instead of simulating a strategy bar by bar, it represents prices, signals and positions as NumPy arrays, broadcasts your parameter grid across extra array dimensions, and compiles the hot loops with Numba to run at near-C speed. One backtest and ten thousand backtests are, to the machine, almost the same operation.

This tutorial covers installation, loading data, running a moving-average crossover through Portfolio.from_signals, reading the stats, the parameter-sweep workflow that is vectorbt's genuine killer feature, plotting, how it compares with Backtrader, the free-versus-PRO split, and the sharp edges - because vectorised backtesting makes it easier to fool yourself, not harder.


Installation and Data

vectorbt installs from PyPI and pulls in NumPy, pandas, Numba and Plotly. Data can come from anywhere that produces a pandas Series or DataFrame; the built-in YFData wrapper fetches from Yahoo Finance via yfinance, which is fine for learning, though for serious work you will want a proper financial data API with a contract behind it.

pip install vectorbt yfinance
import vectorbt as vbt # Daily closes for the S&P 500 ETF, 2016 to end-2025 price = vbt.YFData.download( "SPY", start="2016-01-01", end="2025-12-31", ).get("Close") print(price.tail()) print(len(price), "daily bars")

No API key is needed for Yahoo data. The first vectorbt call in a session is slower than you expect - Numba compiles functions on first use - and every subsequent call is fast. That one-off compile pause surprises everyone once.


A Crossover Backtest in Five Lines

The strategy: hold SPY when its 20-day moving average is above its 100-day moving average, be flat otherwise. In an event-driven framework this is a class with lifecycle methods; in vectorbt it is three array operations and a portfolio constructor.

fast = vbt.MA.run(price, window=20) slow = vbt.MA.run(price, window=100) entries = fast.ma_crossed_above(slow) # boolean Series: True on cross up exits = fast.ma_crossed_below(slow) # True on cross down pf = vbt.Portfolio.from_signals( price, entries, exits, init_cash=10_000, fees=0.001, # 10 bps per trade freq="1D", # needed for annualised metrics ) print(pf.stats())

Portfolio.from_signals turns boolean entry and exit arrays into simulated positions, cash and equity, applying fees and (optionally) slippage as it goes. The stats() call prints a full tear sheet: total return, benchmark return, Sharpe ratio, maximum drawdown, win rate, trade counts, average duration and more. The individual metrics are also methods when you want them programmatically:

print(f"Total return: {pf.total_return():.1%}") print(f"Sharpe ratio: {pf.sharpe_ratio():.2f}") print(f"Max drawdown: {pf.max_drawdown():.1%}") print(f"Trades: {pf.trades.count()}")

Always price your strategy against the do-nothing alternative. vectorbt makes the comparison one line, because holding the asset is itself a portfolio:

hold = vbt.Portfolio.from_holding(price, init_cash=10_000, freq="1D") print(f"Crossover: {pf.total_return():.1%} | Buy and hold: {hold.total_return():.1%}") print(f"Crossover DD: {pf.max_drawdown():.1%} | Hold DD: {hold.max_drawdown():.1%}")

For a run like this you might see the crossover lagging buy-and-hold on total return while roughly halving the drawdown - the classic trend-following trade-off, and a useful sanity check that the simulation is behaving. A strategy that cannot beat from_holding on any risk-adjusted measure has no reason to exist, and it takes ten seconds to find out. What counts as a good Sharpe or an acceptable drawdown is a strategy-design question, covered in our quant trading strategies guide; vectorbt just measures it quickly.

Under the hood these portfolio objects are thin wrappers over NumPy arrays, which is why everything from indicator computation to trade accounting runs at array speed. If broadcasting and vectorised thinking are new to you, our NumPy for quantitative finance guide covers the mental model vectorbt is built on.


The Killer Feature: Sweeping Thousands of Parameters

Everything so far, Backtrader also does. Here is what it cannot do in any reasonable time. Pass arrays of windows instead of single values, and vectorbt broadcasts the entire backtest across every combination:

import numpy as np windows = np.arange(2, 101) # 2, 3, ..., 100 fast, slow = vbt.MA.run_combs( price, window=windows, r=2, short_names=["fast", "slow"], # all pairs with fast < slow ) entries = fast.ma_crossed_above(slow) exits = fast.ma_crossed_below(slow) pf = vbt.Portfolio.from_signals( price, entries, exits, init_cash=10_000, fees=0.001, freq="1D", ) sharpe = pf.sharpe_ratio() # Series with 4,851 entries print(sharpe.sort_values(ascending=False).head(10))

run_combs generates every fast/slow pair (4,851 of them for 99 windows), and entries, exits and the portfolio are now two-dimensional: time along one axis, parameter combinations along the other. The Sharpe call returns one value per combination, as a pandas Series indexed by the window pair. On a decade of daily data this completes in seconds.

The right way to read the output is as a landscape, not a leaderboard:

sharpe.vbt.heatmap( x_level="fast_window", y_level="slow_window", symmetric=False, ).show()

A broad plateau of decent Sharpe ratios across neighbouring windows suggests the effect is real; a single glowing cell surrounded by mediocrity is noise that got lucky, and picking it is curve-fitting. This is the honest use of the sweep feature: mapping how sensitive a strategy is to its parameters, rather than mining for the best cell. The same broadcasting works across assets too - pass a DataFrame of prices and vectorbt happily runs every parameter set against every column, which is how you check a rule across a whole universe in one call.

Plotting beyond heatmaps is built in as well. pf.plot().show() renders an interactive Plotly dashboard of equity curve, drawdowns and trade markers for any single combination (select one with pf[(20, 100)] first when the portfolio holds many). And because pf.returns() hands back an ordinary pandas Series, the wider Python reporting ecosystem plugs straight in: the QuantStats library, for instance, will turn that Series into a full HTML tear sheet with rolling Sharpe, monthly return tables and drawdown analysis in one call. vectorbt does the heavy simulation; presentation can live wherever you like.


Stops, Position Sizing and Other Realism Knobs

from_signals is more capable than the minimal examples suggest, and a few of its arguments close most of the gap between a toy and a defensible simulation.

pf = vbt.Portfolio.from_signals( price, entries, exits, init_cash=10_000, size=0.95, # invest 95% of equity per entry size_type="percent", fees=0.001, # 10 bps commission slippage=0.0005, # 5 bps adverse fill assumption sl_stop=0.05, # 5% stop loss from entry price tsl_stop=0.10, # 10% trailing stop tp_stop=0.15, # 15% take profit freq="1D", ) print(pf.stats()[["Total Return [%]", "Max Drawdown [%]", "Total Trades"]])

The stop arguments generate synthetic exit signals inside the Numba simulation, evaluated bar by bar against the entry price, so they remain fast even across a full parameter grid - you can sweep sl_stop as an array exactly like a moving-average window. Percentage-of-equity sizing compounds positions realistically instead of trading a fixed quantity forever.

Two limits to keep in mind. Stops are checked against bar closes (or the OHLC columns you supply) rather than a true intrabar path, so a stop and a take-profit that both trigger inside one bar resolve by assumption, not by fact. And once your logic needs to consult the portfolio's own state before deciding (scale in only if the last trade won, halve size after a drawdown), you have outgrown from_signals and want from_order_func, vectorbt's escape hatch into custom Numba-compiled order logic - powerful, but at that point an event-driven framework is often the more natural home.


vectorbt vs Backtrader

The two libraries answer different questions, and the comparison is less "which is better" than "which failure mode do you prefer". Our Backtrader tutorial shows the event-driven side in full; the short version:

vectorbtBacktrader
ModelVectorised arrays, Numba-compiledEvent-driven, bar by bar
Parameter sweepsThousands in secondsHours; one run per combination
Complex order logicAwkward (arrays, or custom Numba)Natural (it is just Python in next())
Path-dependent sizing, stopsPossible but fiddlyStraightforward
Live tradingNoYes (IB, Oanda)
Development statusOpen-source in maintenance; PRO activeMinimal since 2020

Event-driven wins whenever the decision at bar t genuinely depends on your own state in a way that resists precomputation: scaling into positions, trailing stops with intrabar logic, order-queue realism, portfolio-level risk rules that gate individual trades. It is also the shape that transfers to live execution, since real trading is an event loop.

Vectorised wins for research throughput: signal ideas, parameter maps, cross-sectional tests over hundreds of assets. A workflow many quants settle into is vectorbt to explore the idea space cheaply, then an event-driven reimplementation of the survivors to check that realistic execution does not kill them. If the two disagree, believe the event-driven one. The broader field, including QuantConnect and Zipline's descendants, is mapped in our backtesting platforms comparison.


Free vectorbt vs vectorbt PRO

The open-source vectorbt this tutorial uses is complete and functional, but it is in maintenance mode: Polakow's active development moved to vectorbt PRO, a commercial rewrite distributed to subscribers through a private repository (access is sold as a recurring subscription; pricing is on vectorbt.pro and has changed over time, so check it directly). PRO adds a redesigned data layer, more realistic order simulation, better documentation and substantially more indicator and analysis tooling.

For learning and most personal research, the free version is plenty, and everything in this tutorial runs on it. Consider PRO when vectorbt has become your daily driver and its documentation, simulation fidelity or support gaps start costing you real time. Be aware the two APIs have diverged: PRO code samples do not always run on free vectorbt, which matters when you are searching for help online.


Where Vectorised Backtesting Bites

vectorbt makes iteration fast, and fast iteration manufactures overconfidence at scale. These caveats are the difference between using the tool and being used by it.

Lookahead bias is easier to commit here. In an event-driven framework, future data is structurally hard to reach; in an array world, it is one indexing mistake away. Classics include normalising a signal by the full sample's mean and standard deviation (which leaks the future into every bar), and acting on a signal computed from the same bar's close. By default from_signals executes on the close of the signal bar - defensible for slow signals, flattering for fast ones. Shifting entries by one bar (entries.vbt.signals.fshift() or a plain pandas shift) and comparing results is a cheap honesty test; if the shift destroys the strategy, you never had one.

A 4,851-run sweep is 4,851 chances to overfit. The best cell in any large grid has a wonderful backtest by construction. Hold out data the sweep never touched, prefer plateaus to peaks, and treat the top of the leaderboard as a hypothesis, not a result.

Fills are idealised. Free vectorbt fills whole orders at the bar's close (or your chosen price column) plus a fixed fee and slippage percentage. There is no order book, no partial fills, no intrabar path - a stop and a target hit within the same bar cannot be sequenced correctly from OHLC data alone. Strategies whose economics live inside the bar need finer data or an event-driven engine.

Yahoo data is a convenience, not a source of record. Survivorship-free universes, clean corporate actions and reliable intraday history all require a proper data vendor.

Operational quirks. First-call Numba compilation adds seconds; large broadcasts can exhaust memory (a 5,000-combination sweep over 500 assets of minute data is billions of array elements - chunk it); and pinning your vectorbt version is wise, since the free package evolves slowly while online examples target various eras of the API.

None of this argues against the tool. It argues for respecting what it optimises: vectorbt compresses the cost of asking questions, and does nothing whatsoever to improve the quality of your answers.

Ask cheap questions, then check the survivors expensively. That is the workflow vectorbt is for.

Skip the £25k programme - try the alternative

Master's programmes are slow and expensive. Quantt is a self-paced alternative. Start free with a real lesson and interview practice, then unlock 50+ courses and your personalised plan.

Free to start · Or code free in the browser