◆ New — Strategy Validator, Code Fixer & Prop Compliance Checker are live. Try one free →
Home / Learn / backtrader vs vectorbt vs freqtrade: Which Python Backtester?
Python

backtrader vs vectorbt vs freqtrade: Which Python Backtester?

backtrader is an event-driven engine, vectorbt is a vectorized array engine, and freqtrade is a full crypto bot framework with a backtester attached; each trades off speed, realism, and how easy it is to leak look-ahead. This honest comparison lays out where each fits and the specific footguns of each, with no notion of a single best tool. It is educational content about tooling and backtest correctness only, not trading or financial advice, and makes no claim about profitability.

Which Python backtester should I use?

There is no single best answer, and any article that gives you one is selling something. The honest framing is a trade-off between three axes: raw speed, execution realism, and how hard it is to accidentally leak future information. backtrader is an event-driven engine that steps through bars one at a time, which makes realistic order handling natural but large sweeps slow. vectorbt is a vectorized engine built on NumPy and pandas that runs thousands of parameter combinations in seconds, at the cost of a programming model where look-ahead is easier to introduce. freqtrade is not primarily a backtester at all; it is a complete crypto trading bot with a backtesting mode, so it gives you live-trading plumbing and a strict signal contract, but you inherit its whole framework whether you want it or not.

A reasonable default heuristic: reach for vectorbt when you need to explore a large parameter space quickly, backtrader when execution detail (stops, brackets, partial fills, multi-asset sizing) matters more than speed, and freqtrade when your actual goal is to deploy a crypto bot and you want backtest and live to share one codebase. None of these choices makes a strategy work; they only change how faithfully you measure it and how likely you are to fool yourself. As [why most backtests fail](/learn/why-most-backtests-fail) argues, the engine is rarely the reason a backtest is wrong. The reason is usually look-ahead or overfitting, and every engine below can be misused into both.

backtrader: event-driven realism, one bar at a time

backtrader processes data as a stream of events. On each bar its next() method is called, and inside it you see the world only as it existed up to and including the current bar. That architecture makes realistic execution the path of least resistance: an order you submit on bar t is, by default, filled on the next bar's open, which is exactly the decide-now-execute-later timing you want.

The framework's biggest footgun is line indexing. In backtrader, self.data.close[0] is the current bar, and self.data.close[-1] is the previous bar. A positive index like self.data.close[1] reaches into the future and is a look-ahead bug.

# ✅ CORRECT: read current and past bars only
import backtrader as bt

class Cross(bt.Strategy):
    def __init__(self):
        self.fast = bt.ind.SMA(self.data.close, period=10)
        self.slow = bt.ind.SMA(self.data.close, period=30)

    def next(self):
        # [0] = current bar, [-1] = previous bar. Never use [1].
        if self.fast[0] > self.slow[0] and self.fast[-1] <= self.slow[-1]:
            if not self.position:
                self.buy()   # fills on the NEXT bar's open by default
        elif self.fast[0] < self.slow[0] and self.position:
            self.close()

Because the buy fills on the next bar, the timing is honest without any manual shifting. The cost is speed: stepping bar-by-bar in Python is slow, and a large grid search can take minutes to hours. backtrader is the right tool when you value that execution fidelity, including stops and bracket orders, over sweep throughput.

vectorbt: vectorized speed, easier to leak

vectorbt represents prices and signals as aligned arrays and computes the whole backtest in vectorized NumPy, so it is dramatically faster than event-driven engines for parameter sweeps. That speed is its selling point and its danger: because everything is an array operation, the timing between "signal computed" and "order filled" is implicit, and the default Portfolio.from_signals fills at the same bar's close. If your entries are computed from that same close, you have leaked look-ahead without a single obviously wrong line.

The fix is to lag signals explicitly and set the fill price:

# ✅ CORRECT: shift signals so decision on bar t executes on bar t+1
import vectorbt as vbt

fast = close.rolling(10).mean()
slow = close.rolling(30).mean()
raw_entries = (fast > slow) & (fast.shift(1) <= slow.shift(1))
raw_exits = (fast < slow) & (fast.shift(1) >= slow.shift(1))

entries = raw_entries.shift(1).fillna(False)   # past forward = safe
exits = raw_exits.shift(1).fillna(False)

pf = vbt.Portfolio.from_signals(
    close, entries, exits, price=close,
    fees=0.0005, slippage=0.0005, freq="1D",
)

The other vectorbt-adjacent footgun is normalization. Because you are working with whole arrays, it is tempting to write something like df['x'] / df['x'].max(), which divides by a statistic computed over the entire series, including the future. Use a trailing or expanding window instead (df['x'] / df['x'].expanding().max()), or fit the scale on the training slice only. The dedicated [look-ahead in vectorbt](/learn/vectorbt-look-ahead-bias) article covers the same-bar fill in detail, and the [Python Backtest Validator](/python) flags both the unshifted from_signals and the whole-series divisor automatically.

freqtrade: a bot framework that happens to backtest

freqtrade is a full crypto trading bot: it manages exchange connections, wallets, pairlists, and live order execution, and its backtester exists so that what you backtest is close to what you deploy. That shared codebase is its real advantage. The strategy contract is opinionated: you populate indicator, entry, and exit columns on a dataframe, and freqtrade enforces a strict rule that entries and exits are acted on at the open of the next candle, which structurally reduces one common class of look-ahead.

That guardrail is not total. The framework's own documentation warns about look-ahead and recabling bias, and the classic freqtrade footgun is computing an indicator that peeks across the whole dataframe. Anything centered or forward-looking in populate_indicators leaks.

# ✅ CORRECT: indicators use only trailing information
def populate_indicators(self, dataframe, metadata):
    dataframe["sma_fast"] = dataframe["close"].rolling(10).mean()
    dataframe["sma_slow"] = dataframe["close"].rolling(30).mean()
    return dataframe

def populate_entry_trend(self, dataframe, metadata):
    cond = (
        (dataframe["sma_fast"] > dataframe["sma_slow"]) &
        (dataframe["sma_fast"].shift(1) <= dataframe["sma_slow"].shift(1))
    )
    dataframe.loc[cond, "enter_long"] = 1
    return dataframe

What you must never write is a centered rolling window such as rolling(20, center=True).mean(), which averages future bars into the current one, or any .shift(-1) in an indicator. freqtrade even ships a lookahead-analysis command to help catch these, but the discipline is still on you. Choose freqtrade when deployment to a supported exchange is the actual goal; do not adopt its entire framework just to run a quick research sweep you could do in a few lines of vectorbt.

Side-by-side, with the honest caveats

The table below summarizes the trade-offs. Treat it as orientation, not a verdict.

| Dimension | backtrader | vectorbt | freqtrade | |---|---|---|---| | Model | Event-driven, bar-by-bar | Vectorized arrays | Bot framework + backtester | | Speed on big sweeps | Slow | Very fast | Moderate | | Execution realism | High (stops, brackets, partial fills) | Depends on your setup | Good, geared to crypto/next-candle | | Default fill timing | Next bar open (safe) | Same-bar close (leaky if unshifted) | Next candle open (safe-by-default) | | Main look-ahead footgun | Positive line index close[1] | Unshifted from_signals, whole-series stats | Centered/forward indicators in populate_* | | Best when | Execution detail matters | Large parameter exploration | You intend to deploy a crypto bot |

A few caveats the table cannot hold. First, none of these numbers are benchmarks. "Fast" and "slow" are relative orders of magnitude, not measurements, and your data size and hardware dominate. Second, every one of these engines can be driven into look-ahead; the defaults differ, but correctness is ultimately your responsibility, which is why the same line-level rules from [pandas .shift() for trading signals](/learn/pandas-shift-trading-signals) apply across all three. Third, and most important, choosing the right engine does not make a strategy good. A leak-free, realistic backtest can still be hopelessly overfit, so pair whichever engine you pick with [walk-forward analysis](/learn/walk-forward-analysis), [in-sample vs out-of-sample](/learn/in-sample-vs-out-of-sample) testing, the [overfitting checker](/backtest/overfitting), and a pass through the [Python Backtest Validator](/python). This comparison is educational, about tooling and correctness only. It is not financial advice and makes no claim about profitability or returns.

Key takeaways

Educational only — not financial advice. Trading involves substantial risk of loss.

Catch the bug that compiles.Run auditGet Pro