# Python backtesting libraries compared on one strategy

URL: https://fincodelabs.com/en/blog/backtesting-python-libraries · Publisher: Fincode (Fincode Yazılım ve Teknoloji A.Ş.), Istanbul, Turkey · Category: Backtesting and risk · Published: 2026-10-25 · Updated: 2026-11-05

Python backtesting: backtesting.py, backtrader, vectorbt, zipline-reloaded and bt compared by license and maintenance, with walk-forward and Monte Carlo code.

**Short answer:** **Backtesting in Python** is either vectorized, computing whole pandas or NumPy columns at once, or event-driven, feeding bars one at a time to logic that behaves like a live system. The main free libraries, backtesting.py, backtrader, vectorbt, zipline-reloaded and bt, differ in license, maintenance, portfolio support and cost modeling. None is best for every case: choose by those criteria, verify against plain pandas and test out of sample; a backtest does not guarantee future results.

Which Python backtesting library fits depends on what you trade, how you plan to go live and which license your product can accept. This guide compares five libraries from their documentation, PyPI and GitHub pages as of 25 October 2026, then runs one strategy in plain pandas and in four of them on synthetic data, with walk-forward and Monte Carlo checks. zipline-reloaded was compared from its documentation only and was not run.

Why backtests flatter strategies is covered in [backtesting pitfalls](https://fincodelabs.com/en/blog/backtesting-trading-strategy-pitfalls); this guide is about tools.

## Vectorized vs event-driven backtesting in Python

A vectorized backtest computes signals and returns for the whole history with array operations; an event-driven backtest feeds bars one at a time to strategy code that reacts as a live system would.

**Two ways to backtest**

| Aspect | Vectorized | Event-driven |
| --- | --- | --- |
| Speed | Fast; suits parameter sweeps | Slower per run |
| Order logic | Stops, limits and partial fills are awkward | Natural: code reacts to each bar and fill |
| Look-ahead risk | One missing shift leaks future data | Lower, but not zero |
| Path to live trading | Logic is usually rewritten | Some frameworks reuse the same callbacks live |
| Examples | Plain pandas, vectorbt | backtrader, zipline-reloaded; backtesting.py supports both styles |

## Backtesting Python libraries compared (October 2026)

The five libraries differ less in what they can simulate than in license, maintenance and portfolio support.

**Free Python backtesting libraries, checked on 25 October 2026**

| Library | Latest PyPI release | License | Style | Several instruments | Costs | Maintenance signal |
| --- | --- | --- | --- | --- | --- | --- |
| backtesting.py | 0.6.6, 22 July 2026 | AGPL-3.0 | Strategy class or precomputed signals | One per Backtest; MultiBacktest runs several separately | Commission (rate, fixed plus rate, or function), spread | Last commit August 2026 |
| backtrader | 1.9.78.123, 19 April 2023 | GPL-3.0 | Event-driven | Many data feeds and timeframes | Commission schemes, slippage | No release or master commit since April 2023; not archived |
| vectorbt | 1.2.0, 19 October 2026 | Apache 2.0 with Commons Clause | Vectorized (NumPy, Numba) | Columns, optional cash sharing | Fees, slippage | Active; limit orders and leverage are in the separate, paid VectorBT PRO |
| zipline-reloaded | 3.2.0, 18 October 2026 | Apache-2.0 | Event-driven | Data bundles on a trading calendar | Commission and slippage models | Fixes only; new features go to ML4T Backtest |
| bt | 1.4.1, 10 October 2026 | MIT | Algorithm stacks: schedule, select, weigh, rebalance | Portfolio trees of securities and strategies | Commissions, transaction cost models | Last commit October 2026 |

Release dates are PyPI upload dates; maintenance signals come from GitHub. Recheck both before committing. Activity signals upkeep, not quality.

## Licenses: AGPL, GPL and Commons Clause in practice

For a commercial product, license terms can decide before features do.

- **AGPL-3.0 (backtesting.py).** Section 13: if you modify it and users interact with it remotely, your version must offer them its source code.
- **GPL-3.0 (backtrader).** Distributing modified versions or software built on it brings source-sharing obligations.
- **Apache 2.0 with Commons Clause (vectorbt).** Source-available ("fair-code"), not OSI open source: free to use, but no right to sell a product or service whose value derives entirely or substantially from it, including paid hosting, consulting or support for it.
- **MIT (bt) and Apache-2.0 (zipline-reloaded).** Permissive, with notice requirements.

Internal research rarely triggers these clauses; a hosted product or client deliverable can. This is not legal advice.

## backtesting.py vs backtrader: how to choose a framework

Neither is better in general; decide by these criteria, then test the candidate on a strategy you can verify.

1. **Asset scope.** One instrument per run (backtesting.py) or several feeds and timeframes together (backtrader, zipline-reloaded, bt, vectorbt columns).
2. **Order realism.** backtrader lists market, limit, stop, stop-limit, trailing, OCO and bracket orders; the free vectorbt leaves limit orders to VectorBT PRO, a separate membership product.
3. **Speed.** Large parameter sweeps favor vectorized tools; measure on your own data.
4. **Path to live trading.** backtrader documents live connections to Interactive Brokers, Oanda v1 and VisualChart, plus third-party ones such as ccxt; otherwise live orders need a separate engine, such as a [CCXT Python bot](https://fincodelabs.com/en/blog/ccxt-python-trading-bot).
5. **Units and markets.** backtesting.py trades whole units unless you use FractionalBacktest, which matters for crypto; zipline-reloaded uses an exchange trading calendar, NYSE by default.
6. **Maintenance and license**, as in the table above.

## Crypto backtesting in Python: what changes

Crypto backtesting in Python uses the same libraries, but five market details need explicit handling, or the results flatter the strategy.

- **24/7 sessions.** Annualized statistics depend on the assumed day count. backtesting.py's statistics code, read on 4 November 2026, uses 365 days when enough of the index falls on weekends and 252 otherwise. Check what your tool assumes.
- **Funding.** Perpetual futures positions pay or receive [funding](https://fincodelabs.com/en/blog/perpetual-futures-funding-rate) at set intervals. Model it as a separate cash flow; a commission setting does not capture it.
- **Delisted pairs.** A universe built from today's symbol list leaves out pairs removed since, which is survivorship bias. Build the list as of each test date.
- **Candle gaps.** Periods without trades, outages and maintenance can leave missing candles. Check for missing timestamps, because forward-filling silently invents prices you could not have traded.
- **Fee tiers.** Fees vary with volume tier, maker or taker status and discounts. Test with the rate your account actually pays, and with the taker rate when orders cross the spread.

### Crypto backtesting data, software and GitHub projects

Data usually starts with candles from an exchange's API or public files, as covered in [crypto price APIs](https://fincodelabs.com/en/blog/crypto-price-api). On GitHub, some crypto bots include a built-in tester. Freqtrade's documentation, read on 4 November 2026, says it uses the exchange's default fees and assumes orders fill at the requested price, with no slippage, while the price is within the candle's range. Read such assumptions in any tool before trusting its figures; a backtest does not guarantee future results.

## The same strategy in plain pandas and four libraries

Before trusting a library, reproduce one simple strategy in plain pandas and compare the trades.

The strategy is long while a 10-day simple moving average is above a 30-day one, filling at the next open with 0.1% commission per side, on 2,000 synthetic daily bars (run on 5 November 2026 with Python 3.11, pandas 3.1.1, NumPy 2.4.6 and backtesting.py 0.6.6):

```python
import numpy as np
import pandas as pd

rng = np.random.default_rng(7)                       # synthetic, hypothetical prices
n = 2000
close = 100 * np.exp(np.cumsum(rng.normal(0.0002, 0.012, n)))
open_ = np.r_[100, close[:-1]] * np.exp(rng.normal(0, 0.002, n))
high = np.maximum(open_, close) * (1 + rng.uniform(0, 0.006, n))
low = np.minimum(open_, close) * (1 - rng.uniform(0, 0.006, n))
df = pd.DataFrame({"Open": open_, "High": high, "Low": low, "Close": close},
                  index=pd.date_range("2019-01-01", periods=n, freq="D"))

def bar_returns(df, fast, slow, fee=0.001):
    """Long-only SMA cross: signal at the close, fill at the next open, fee per side."""
    f, s = df.Close.rolling(fast).mean(), df.Close.rolling(slow).mean()
    pos = (f > s).astype(int).shift(1).fillna(0)
    r = (df.Open.shift(-1) / df.Open - 1).fillna(0)
    return (1 + pos * r) * (1 - fee) ** pos.diff().abs().fillna(0) - 1, pos

ret, pos = bar_returns(df, 10, 30)
print("trades:", int((pos.diff() == 1).sum()), "ending equity:", round((1 + ret).prod(), 4))
```

```
trades: 38 ending equity: 0.729
```

The same rules in backtesting.py, continuing with df:

```python
from backtesting import Backtest, Strategy
from backtesting.lib import crossover

def SMA(values, n):
    return pd.Series(values).rolling(n).mean()

class SmaCross(Strategy):
    fast, slow = 10, 30
    def init(self):
        self.f = self.I(SMA, self.data.Close, self.fast)
        self.s = self.I(SMA, self.data.Close, self.slow)
    def next(self):
        if crossover(self.f, self.s):
            self.buy()                    # market order: fills at the next open
        elif crossover(self.s, self.f):
            self.position.close()

stats = Backtest(df, SmaCross, cash=1_000_000, commission=0.001,
                 finalize_trades=True).run()
print("trades:", stats["# Trades"], "ending equity:", round(stats["Equity Final [$]"] / 1e6, 4))
```

```
trades: 38 ending equity: 0.7283
```

Both make the same 38 entries, and the 37 closed trades exit, at identical prices. The small equity gap comes mainly from the last trade, which backtesting.py closes on the final bar with exit commission (finalize_trades=True) while pandas leaves it open; whole-unit sizing adds a little. A different trade count would point to a fill-timing or warm-up difference to fix first. The strategy lost about 27% on random-walk synthetic data.

The same rule, data and costs in three more libraries, run on the same day, make the same trades:

**One strategy in five tools, on synthetic data, 5 November 2026 (no ranking implied)**

| Tool | Entries | Ending equity | Open position marked at | Setup and notes |
| --- | ---: | ---: | --- | --- |
| Plain pandas | 38 | 0.729 | Last open | The reference above |
| backtesting.py 0.6.6 | 38 | 0.7283 | Final bar, with exit commission | Whole units |
| backtrader 1.9.78.123 | 38 | 0.7214 | Last close | Cheat-on-open, order size of cash times 0.9999 divided by open times 1.001 |
| bt 1.4.1 | 38 | 0.729 | Last open | Fed open prices with WeighTarget and Rebalance; re-weighting made 57 buy transactions |
| vectorbt 1.2.0 | 38 | 0.7213 | Last close | from_signals with signals shifted one bar and price set to the open; 0.729 at the last open |

All 38 entry dates and all 37 closed-trade exit dates match plain pandas in each library, so the gaps in ending equity come from where the last open position is marked, plus the exit commission and whole units in backtesting.py. In backtrader, sizing all in without the 0.01% cash reserve had all 38 orders rejected as margin, leaving ending equity at 1.0, a silent failure that this kind of comparison catches. zipline-reloaded was not run, so its row in the table above rests on documentation only.

## Walk-forward analysis in Python

Walk-forward analysis picks parameters on a past window, applies them unchanged to the next window and reports only those out-of-sample results.

```python
from itertools import product

grid = list(product((5, 10, 20), (30, 50, 100)))      # (fast, slow)
curves = {p: bar_returns(df, *p)[0] for p in grid}   # indicators use past data only
growth = lambda r: float((1 + r).prod())

train, test, oos = 500, 250, []
for start in range(0, n - train - test + 1, test):
    fit = slice(start, start + train)
    best = max(grid, key=lambda p: growth(curves[p].iloc[fit]))   # chosen on the past
    oos.append(curves[best].iloc[start + train:start + train + test])
    print(df.index[start + train].date(), best, round(growth(oos[-1]), 3))

period = slice(train, train + len(pd.concat(oos)))
hindsight = max(grid, key=lambda p: growth(curves[p].iloc[period]))
print("walk-forward:", round(growth(pd.concat(oos)), 3),
      "| hindsight best", hindsight, round(growth(curves[hindsight].iloc[period]), 3))
```

```
2020-05-15 (20, 100) 1.03
2021-01-20 (20, 100) 1.128
2021-09-27 (5, 50) 0.891
2022-06-04 (5, 50) 0.974
2023-02-09 (5, 50) 1.031
2023-10-17 (20, 100) 0.959
walk-forward: 0.997 | hindsight best (5, 50) 1.211
```

Each fold optimizes nine SMA pairs on 500 bars and trades the winner for the next 250. The six stitched out-of-sample periods end at 0.997, roughly flat, while the best pair in hindsight over the same 1,500 bars shows +21%. That gap is selection bias: an optimizer reports the hindsight number, while walk-forward measures the selection procedure honestly. Computing indicators on the full series is safe here only because rolling means use past bars; fitted models or scalers must be refitted inside each training window. Walk-forward testing across many symbols is part of the [BIST strategy backtesting platform](https://fincodelabs.com/en/projects/bist-strategy-backtesting-platform) scenario.

## Monte Carlo backtesting in Python: resampling trades

Monte Carlo resampling draws many alternative sequences from the strategy's own trades to show the range of outcomes and drawdowns they could produce.

```
r = stats["_trades"]["ReturnPct"].to_numpy()          # 38 net trade returns
rng = np.random.default_rng(1)
paths = 1 + rng.choice(r, size=(10_000, r.size), replace=True)   # bootstrap
equity = np.cumprod(paths, axis=1)
peak = np.maximum.accumulate(np.hstack([np.ones((10_000, 1)), equity]), axis=1)[:, 1:]
max_dd = (1 - equity / peak).max(axis=1)
print("final equity 5/50/95%:", np.percentile(equity[:, -1], [5, 50, 95]).round(3))
print("max drawdown 5/50/95%:", np.percentile(max_dd, [5, 50, 95]).round(3))
print("paths ending above 1:", (equity[:, -1] > 1).mean().round(3))
```

```
final equity 5/50/95%: [0.404 0.717 1.409]
max drawdown 5/50/95%: [0.223 0.418 0.623]
paths ending above 1: 0.2
```

> **Reading the results (hypothetical data).** Resampling the 38 trades 10,000 times gives a median ending equity of 0.717 and a median maximum drawdown of 42%; one path in twenty fell more than 62% from its peak, and one in five ended with a gain. The actual sequence had a 37% trade-level drawdown, so the historical path alone understates the risk.

Bootstrapping assumes independent trades; if results cluster, resample blocks of consecutive trades or returns.

Libraries reproduce rules; they cannot tell you whether the rules will make money, backtests do not guarantee future results, and automated trading can lose money. Strategies ported from TradingView need the checks in the [TradingView Strategy Tester](https://fincodelabs.com/en/blog/tradingview-strategy-tester) guide and [Pine Script to Python](https://fincodelabs.com/en/blog/pine-script-to-python). Test software built from rules the customer wrote is part of the [custom trading bot development](https://fincodelabs.com/en/services/algorithmic-trading) service.

## Frequently asked questions

### What is the best Python library for backtesting?

None is best for every case. Choose by asset scope, order realism, speed, license, maintenance and path to live trading.

### Is backtesting.py free for commercial use?

It is AGPL-3.0: if you modify it and offer it to users over a network, you must give them your modified source. Have the terms reviewed for a commercial product.

### Is backtrader still maintained?

Its last PyPI release, 1.9.78.123, and last master-branch commit date from 19 April 2023 (checked 25 October 2026); the repository is not archived.

### How do I do walk-forward analysis in Python?

Optimize parameters on a rolling past window, trade them unchanged on the next one, and report only the stitched out-of-sample periods.

### Does a Monte Carlo test prove a strategy works?

No. It shows the range of results the observed trades could produce in another order or mix, but it cannot add an edge the trades lack.

### What is different about crypto backtesting?

Markets run around the clock, perpetual futures add funding, delisted pairs vanish from current symbol lists, thin pairs leave candle gaps and fees depend on your tier.

## Sources

- [backtesting.py API reference: Backtest (parameters, fills, commission, optimize)](https://kernc.github.io/backtesting.py/doc/backtesting/backtesting.html)
- [backtesting.py API reference: lib (MultiBacktest, FractionalBacktest, SignalStrategy)](https://kernc.github.io/backtesting.py/doc/backtesting/lib.html)
- [backtesting.py on GitHub: LICENSE (GNU AGPL-3.0, section 13)](https://github.com/kernc/backtesting.py/blob/master/LICENSE.md)
- [PyPI: backtesting (release 0.6.6)](https://pypi.org/project/backtesting/)
- [backtrader documentation: Features](https://www.backtrader.com/home/features/)
- [backtrader on GitHub (GPL-3.0, commit history, version 1.9.78.123 commit of 19 April 2023)](https://github.com/mementum/backtrader)
- [vectorbt on GitHub (README, PRO features, Apache 2.0 with Commons Clause)](https://github.com/polakowo/vectorbt)
- [vectorbt API reference: Portfolio (fees, slippage, cash sharing)](https://vectorbt.dev/api/portfolio/base/)
- [PyPI: vectorbt (release 1.2.0)](https://pypi.org/project/vectorbt/)
- [zipline-reloaded on GitHub (README, maintenance status)](https://github.com/stefan-jansen/zipline-reloaded)
- [Zipline Reloaded documentation: Beginner tutorial (events, bundles, calendars, costs)](https://zipline.ml4trading.io/beginner-tutorial.html)
- [PyPI: zipline-reloaded (release 3.2.0)](https://pypi.org/project/zipline-reloaded/)
- [bt on GitHub (README, MIT license)](https://github.com/pmorissette/bt)
- [backtesting.py source: _stats.py (annual trading days)](https://github.com/kernc/backtesting.py/blob/master/backtesting/_stats.py)
- [Freqtrade documentation: Backtesting (fees and assumptions)](https://www.freqtrade.io/en/stable/backtesting/)

> This guide is general information about software and markets, not investment, legal or tax advice. Automated trading can lose money, past or backtested performance does not guarantee future results, and the rules that apply depend on your jurisdiction and your exchange's terms. Sources, checks and corrections: see the [editorial policy](https://fincodelabs.com/en/editorial-policy).

---

Published by Fincode (Fincode Yazılım ve Teknoloji A.Ş.), a software company in Istanbul, Turkey: https://fincodelabs.com/en/blog/backtesting-python-libraries

Cite as: Fincode, "Python backtesting libraries compared on one strategy", updated November 5, 2026, https://fincodelabs.com/en/blog/backtesting-python-libraries
