Backtesting.py Guide: How to Backtest Trading Strategies in Python
Whether you're checking a simple moving average strategy or a complex algorithmic idea, backtesting.py gives you a straightforward way to test your trading logic with historical data. It's a popular, free Python library that lets you simulate trades to see how an idea might have performed in the past, helping you build confidence before you commit real money.
This walkthrough will take you from getting it installed to fine-tuning a strategy, all using this powerful but user-friendly toolkit.
What Is Backtesting.py?
In simple terms, backtesting.py is a Python library for backtesting trading strategies. You feed it historical price data (like Open, High, Low, Close, and Volume) and the rules of your strategy. It then runs a simulation, executing pretend trades along the historical timeline, and gives you a detailed report on how it would have done.
Think of it this way: if pandas and NumPy are the go-to libraries for data scientists to crunch numbers, backtesting.py is the equivalent for traders to test their ideas. It’s built on top of those very libraries, so it’s fast and works seamlessly in a Python data analysis environment.
What makes it stand out is how simple it is to use. The entire API is clean and intuitive, not bloated with complicated features. You can get a powerful backtest running with just a few lines of code. It also uses Bokeh to create clear, interactive charts of your equity curve and trades, so you can visually understand your results immediately. This is a great first step if your ultimate goal is to run your own Automated Trading Bot TradingView: Complete Guide to Algorithmic Trading Success.
Where Does Backtesting.py Fit In?
Choosing a backtesting library in Python can feel overwhelming—there are a lot of options. It’s not about finding the “best” one, but the one that’s right for you and your current project.
Here’s a quick, honest look at how some popular libraries stack up:
| Library | Strengths | Best For |
|---|---|---|
| backtesting.py | Lightweight, fast, interactive charts | Beginners & intermediate traders |
| VectorBT | Numba-accelerated, NumPy-native | High-frequency / large datasets |
| Backtrader | Highly extensible, large community | Professional-grade complex strategies |
| Zipline Reloaded | Event-driven, Quantopian heritage | Institutional-style pipelines |
| QuantConnect | Cloud-based, live trading capable | Full-stack algo trading |
As you can see, each has its specialty. So, why do so many people start with (and stick to) backtesting.py?
Here’s the simple breakdown:
- It’s completely free. No hidden costs, subscriptions, or tiers. You own your code.
- Works with almost any market. If you have OHLCV data (that’s Open, High, Low, Close, Volume), you can test it—stocks, crypto, forex, you name it.
- Optimizing is a breeze. The built-in SAMBO optimizer lets you quickly test hundreds of parameter combinations to find what might work best.
- Flexible engine. It can handle vectorized calculations for speed, or event-driven logic for more nuanced strategies.
- You actually see what happened. The interactive charts let you zoom right into every trade, see your equity curve, and visually understand why a strategy performed the way it did.
Of course, it’s not magic. It has its limits. For instance, natively testing a portfolio of multiple assets at once is tricky, and all your indicators need to work from that standard OHLCV data. For extremely complex, institutional-grade strategies, you might eventually hit a wall.
But for most people building and testing their own trading ideas? It hits a sweet spot of being powerful enough without making things complicated.
Getting Started with Backtest Py
Setting up backtest py is simple. If you have Python 3 and pip ready to go, you're already halfway there.
To install the library, just run this command in your terminal:
pip install backtesting
After it's installed, you can pull in the main parts you'll need to start building and running your strategies. Here’s a typical starting point for your code:
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from backtesting.test import SMA, GOOG
You'll notice we imported GOOG from backtesting.test. This module includes some example data—like Google's stock price and EUR/USD forex data—so you can test your ideas immediately without needing to find your own data first.
Once you're ready to move past the sample data, you'll need to hook up to a real data source. Many people use libraries like yfinance to pull stock data from Yahoo Finance for free. Other services like Quandl or your preferred broker's API are also great options for getting live or historical market data to test against.
How to Build Your First Trading Strategy
Let's walk through how to create your first trading strategy in backtesting.py. Think of it as a two-step recipe: every strategy you build uses the same basic structure, which makes getting started pretty straightforward.
Every strategy you write will be built on the Strategy base class. At its heart, you just need to define two key parts:
init(): This is your setup zone. It runs once when the backtest starts. You'll use it to calculate any indicators you need—like moving averages or RSI—so everything is ready before the simulated trading begins.next(): This is where the action happens. It’s called for every new candlestick (bar) of data. Your logic for deciding when to buy, sell, or close a position lives here.
A Simple Example: The SMA Crossover
A classic first strategy to try is the Simple Moving Average (SMA) Crossover. The idea is simple: buy when a shorter-term moving average crosses above a longer-term one, and sell (or go short) when the opposite happens.
Here’s the exact code from the documentation. This backtest runs on Alphabet Inc. (Google) stock data over 9 years, starting with $10,000 and including a realistic 0.2% commission per trade.
class SmaCross(Strategy):
n1 = 10
n2 = 20
def init(self):
close = self.data.Close
self.sma1 = self.I(SMA, close, self.n1)
self.sma2 = self.I(SMA, close, self.n2)
def next(self):
if crossover(self.sma1, self.sma2):
self.position.close()
self.buy()
elif crossover(self.sma2, self.sma1):
self.position.close()
self.sell()
bt = Backtest(GOOG, SmaCross, cash=10000, commission=.002)
output = bt.run()
bt.plot()
How Did This Simple Strategy Perform?
Running the backtest gives us concrete numbers to look at. For this specific period and asset, the results were interesting compared to just buying and holding the stock.
Here’s a summary of the key metrics:
| Metric | Strategy Result | Buy & Hold Result |
|---|---|---|
| Total Return | 718% | 607% |
| Sharpe Ratio | 0.72 | — |
| Win Rate | ~55% | — |
This simple 10/20-day SMA crossover strategy ended up with a 718% total return over those 9 years, which outperformed the 607% return from simply buying and holding Google stock for that period. The Sharpe Ratio (a measure of risk-adjusted return) was 0.72, and about 55% of the trades were winners.
It's important to remember: This is a single example on historical data. Past performance, especially from a basic educational strategy, does not guarantee future results. It's a great starting point to learn the framework before testing and developing your own ideas.
Choosing the Right Strategy in Backtesting.py
When you're setting up a backtest, picking the right foundation for your strategy can save you a lot of time. Backtesting.py gives you three main starting points, each suited for a different way of thinking about your trades.
Here’s a straightforward look at your options:
| Strategy Type | Best For | How It Works |
|---|---|---|
Strategy | Full control and custom logic. | You override the init() and next() methods to dictate exactly what happens at each bar, step-by-step. |
TrailingStrategy | Hands-free stop-loss management. | It automatically trails a stop-loss behind your position using a distance based on the Average True Range (ATR). You focus on entries; the exit follows the trend. |
SignalStrategy | Vectorized, signal-based thinking. | Instead of bar-by-bar logic, you prepare arrays of buy/sell signals upfront. It's often simpler and faster for strategies based on indicator crossovers. |
In practice, if your idea is something like "buy when a short moving average crosses above a long one," the SignalStrategy can be the quickest path from idea to result. You just tell it which conditions are your entry and exit signals.
For example, that moving average crossover strategy would look like this:
from backtesting.lib import SignalStrategy
class ExampleStrategy(SignalStrategy):
def init(self):
super().init()
self.set_signal(sma1 > sma2, sma1 < sma2)
Think of it this way: use the base Strategy class when you need intricate rules or money management on every bar. Try TrailingStrategy when you want a simple, automatic way to let your winners run. Opt for SignalStrategy when your logic is mostly about clear, predefined signals. Starting with the right type makes building and testing your idea much smoother.
Putting Your Strategy to the Test and Understanding the Results
Once you’ve built your trading strategy, the next step is to see how it would have performed historically. You do this by passing your strategy to a Backtest and then calling .run(). Think of it like a flight simulator for your trading ideas.
Here’s the simple code to make it happen:
bt = Backtest(your_strategy, your_data)
results = bt.run()
What you get back isn’t just a single number. It’s a detailed report card filled with statistics that help you understand not just if it made money, but how it made money and the risks it took along the way.
Here’s a breakdown of the key numbers you’ll see and what they really tell you:
| Metric | What It Tells You |
|---|---|
| Return [%] | The total profit or loss your strategy generated over the entire test period. |
| Buy & Hold Return [%] | The profit from simply buying and holding the asset. This is your strategy’s benchmark. Did you do better? |
| Sharpe Ratio / Sortino Ratio | Measures your returns relative to the risk (volatility) you took. Higher is generally better. Sortino is similar but only penalizes bad volatility (downside risk). |
| Max Drawdown [%] | The largest peak-to-trough drop in your portfolio value. It’s a gut-check on the worst losing streak you’d have had to sit through. |
| Win Rate [%] | The percentage of your trades that closed for a profit. It doesn't tell the whole story (a 90% win rate can still lose money if the losses are huge). |
| Profit Factor | (Gross Profit / Gross Loss). A factor above 1 means you made more money than you lost. It’s a great quick health check. |
| SQN (System Quality Number) | A single score that tries to gauge the overall robustness and reliability of your trading system. |
Looking at numbers is one thing, but seeing your strategy play out over time is another. That’s where bt.plot() comes in. This command launches a fully interactive chart right in your browser or notebook. You can zoom into any period, see exactly where trades were entered and exited, check if your indicators lined up, and watch your equity curve grow (or shrink) in detail. It’s the best way to visually diagnose what worked and what didn’t.
Finding Your Strategy's Sweet Spot (The Smart Way)
One of the handiest tools in backtesting.py is its built-in optimizer. Think of it as your automated testing partner. Instead of you manually trying out hundreds of different setting combinations and guessing what works best, the optimizer does the heavy lifting. You tell it which parameters to adjust and what your goal is (like getting the best risk-adjusted returns), and it systematically hunts for the best setup.
Here's the basic idea in code:
stats = bt.optimize(
n1=range(5, 30, 5),
n2=range(10, 70, 5),
maximize='Sharpe Ratio',
constraint=lambda p: p.n1 < p.n2
)
In this example, we're asking it to test different values for n1 and n2. The key part is the maximize='Sharpe Ratio' line—that's our target. We want the settings that give us the smoothest, most consistent returns relative to risk. The constraint just makes sure n1 is always less than n2, which makes logical sense for many trading ideas.
The cool part is how it searches. It doesn't just blindly try every single combination (a "grid search"), which can be painfully slow. Instead, it uses an efficient method called SAMBO (Sequential Model-Based Optimization). It learns from each test and intelligently guesses where to look next, saving you a ton of time, especially when you're exploring a wide range of values.
Once it's done, you don't just get a single "best" number. You get heatmaps. These visual charts are incredibly useful because they show you not just the peak, but the whole landscape. You can see if a great result is a lonely, spiky peak (which is risky—it might just be lucky noise) or if it's part of a broad, stable hill (which is more trustworthy). This helps you avoid the trap of picking a setting that's perfectly tuned to past random fluctuations.
You can learn more about the optimizer's capabilities on the official docs: kernc.github.io.
⚠️ Here's the crucial part: The results you get from an optimizer are based on past data. A strategy that looks unbeatable in these tests might just be exceptionally good at fitting that specific historical period, a trick known as curve-fitting. To know if you've really found something solid, you must check how those "optimized" settings perform on fresh, out-of-sample data that wasn't used in the test. If it falls apart, it was just a mirage. This step is non-negotiable for serious testing. For a deeper dive on this practice, algotrading101 has a great guide.
Making the Most of Technical Indicators
Backtesting.py keeps things lean on purpose—it only includes a basic Simple Moving Average (SMA) to show you how indicators work within its system. For building real trading strategies, you'll almost always want to bring in your favorite indicators from other trusted libraries.
The good news is it's straightforward to connect them. You mainly have three great options:
- TA-Lib: This is the classic, go-to library that's been around forever. It's packed with over 150 indicators and is known for being very fast.
- pandas-ta: A newer, popular choice written entirely in Python. It's often easier to install than TA-Lib and has a clean, modern feel.
- Your own custom functions: If you have a special calculation in mind using NumPy or Pandas, you can easily turn it into a compatible indicator.
The key to using any of these within backtesting.py is a special helper called self.I(). Think of it as a universal adapter. It takes your indicator's raw data, ensures it runs at the right time in the backtest, and makes sure it plots neatly on your final chart. For a deep dive into all your indicator options, our guide on TradingView Top 10 Indicators Essential Tools for Technical Analysis Success explores the most powerful tools traders use every day.
For a deeper dive into your options, this guide from algotrading101 is super helpful.
Let's look at a quick, real example. Here’s how you would pull in the Relative Strength Index (RSI) from the popular ta library:
import ta
import pandas as pd
def init(self):
self.rsi = self.I(ta.rsi, pd.Series(self.data.Close), 14)
In this snippet, we're telling the backtesting engine: "Hey, calculate a 14-period RSI using our closing prices, and handle it properly for me."
For more practical examples and a step-by-step walkthrough, the team at greyhoundanalytics has put together an excellent resource.
Q&A Section
Q: Is backtesting.py free to use?
Absolutely. It's completely free and open-source. You can just run pip install backtesting.py and start using it. There are no hidden fees, subscriptions, or licenses to worry about.
Q: What data does backtesting.py support? If you have candlestick data (OHLCV bars), you can use it. This covers pretty much everything: stocks, forex, cryptocurrencies, futures, and more. You just need to get your data into a Pandas DataFrame, which is a common format for this kind of work.
Q: Can I use backtesting.py for live trading? Not directly, no. It's built specifically for testing your ideas on historical data. When you're ready to trade live, you'd use the logic from your strategy with a separate platform or broker API—like Alpaca for stocks, Interactive Brokers, or CCXT for cryptocurrencies.
Q: How does backtesting.py handle commissions and slippage?
You can add trading costs by setting a commission fee when you run your backtest. For slippage (the difference between the price you expect and the price you actually get), the built-in tools are simple. A common workaround is to bump up your commission rate a bit to account for it conservatively.
Q: Is backtesting.py suitable for high-frequency trading (HFT) strategies? For strategies that trade on every tick or every second, probably not. Those require extremely fast, optimized libraries. Backtesting.py works best for strategies on longer timeframes—think minute charts, hourly, or daily. For true HFT, you'd want to look at something like VectorBT, which is built with speed in mind.
Q: How do I prevent overfitting when using the optimizer? The golden rule is to always test your optimized strategy on brand new, unseen data. A good method is walk-forward analysis: tune your parameters on one chunk of history, then validate them on a later period. Also, if you see a parameter setting that's a lonely, crazy spike on a heatmap, it's often a fluke. Look for broader, more stable areas of good performance instead.
Q: I want to backtest my TradingView strategy ideas. Is there a more integrated solution? This is a great question that bridges the gap between idea and execution. While backtesting.py is powerful for Python-based strategies, many traders prefer to develop and test directly within the TradingView ecosystem using Pine Script. For those who want a seamless, no-code solution to build, optimize, and generate professional backtest reports for TradingView strategies, tools like Pineify are purpose-built for this workflow. It allows you to visually create indicators and strategies or use an AI Coding Agent to generate error-free Pine Script, then backtest and analyze the results with institutional-grade reports—all without leaving the platform you're familiar with. You can also learn how to Create New Pine Script on TradingView: A Quick Guide to start coding your own custom indicators for testing.
So you've got the basics of backtesting.py down—what's next? This is where the real fun begins, moving from theory to building something you can actually use.
Think of this as your practical roadmap. Here’s how to go from your first simple backtest to developing more robust trading systems:
- Get it on your machine. First things first, install the library by running
pip install backtesting. Then, don't just read about it—get your hands dirty. Recreate that simple SMA crossover example we talked about, but use a stock or ETF you're personally interested in. - Test with real, current data. A strategy tested on old data is just a history lesson. Connect a library like
yfinanceto pull in live market data. Seeing how your logic performs on recent price action changes the game. - Incorporate real trading indicators. The basic examples are a starting point. Now, integrate a powerhouse like TA-Lib or pandas-ta to build signals with common indicators like RSI, MACD, or Bollinger Bands. This is how you start to encode your actual trading ideas.
- Find your strategy's sweet spot. Use the built-in
bt.optimize()function to test different parameters. Don't just chase the highest profit—look at the optimizer's heatmaps to find parameter zones that are consistently good, not just lucky. Robustness is key. - Explore other tools. Once you're comfortable, remember that
backtesting.pyis one of many great tools. Check out VectorBT if you need blazing speed for complex analysis, or Backtrader if you're focusing on multi-asset portfolio management. Here’s a solid comparison to get you started. - Share and get feedback. Don't develop in a vacuum. Once you have a backtest you're proud of, share it in communities like r/algotrading. Getting peer feedback is the fastest way to spot flaws and refine your edge.
Here’s the single most important piece of advice I can give you: a great backtest result is a starting line, not the finish line. It's the beginning of your research, not a guarantee of future profits.
The path to a trustworthy trading system involves combining that promising backtest with rigorous out-of-sample testing, modeling realistic trading costs and slippage, and applying disciplined position sizing. For a deep dive into doing this right with backtesting.py, the official documentation is your best friend.

