
This simple Python backtesting tutorial will teach you how to build and test your first trading strategy using Python. Python backtesting is one of the most important skills in algorithmic trading. In this tutorial, you'll learn how to backtest a trading strategy with Python using the backtesting.py library from scratch.Imagine you have a trading idea: "When the 20-day moving average crosses above the 50-day moving average, buy. When the opposite happens, sell." It sounds simple, but did it actually work in the past? Is it profitable even after accounting for trading fees? The answer to these questions comes not from guesswork but from scientific backtesting. In this article, we'll take you step-by-step from implementing a simple idea in Python to a fully tested strategy. After reading this, you'll be able to define almost any indicator-based strategy in Python and backtest it across any market and symbol in just a few minutes.
We'll cover four main phases:
By the end, you'll have a working Python file that can test your final strategy on any asset and produce a comprehensive report.
Before we start, we need to prepare our tools. Python is the best choice for this due to its rich libraries and active community.
When learning Python backtesting, one of the first decisions you'll make is choosing a backtesting framework. While several Python libraries are available, backtesting.py library has become one of the most popular choices because it combines simplicity with powerful features. Whether you're testing a basic moving average crossover or a sophisticated algorithmic trading strategy, the library provides everything needed to simulate trades, evaluate performance, and visualize results.Compared with more complex frameworks, backtesting.py is lightweight, easy to learn, and requires only a small amount of code to create professional trading strategies. It integrates naturally with popular Python libraries such as pandas, NumPy, and matplotlib, making it an excellent choice for beginners as well as experienced quantitative traders.Another advantage of backtesting.py library is its interactive reporting system. After running a strategy, the library automatically generates detailed performance statistics, including total return, maximum drawdown, win rate, Sharpe ratio, and an interactive equity curve. These reports make it much easier to analyze a strategy before risking real capital.Throughout this tutorial, we'll use backtesting.py to build, test, and improve a complete Python backtesting workflow that you can later adapt to stocks, cryptocurrencies, forex, or any other financial market.
Python alone isn't enough. We need libraries that handle heavy financial and analytical tasks for us.
| Library | Purpose |
|---|---|
| backtesting.py | Core backtesting engine: strategy building, trade simulation, and reporting |
| pandas & numpy | Data processing and fast numerical calculations |
| pandas-ta | Technical indicators |
| yfinance | Free historical market data from Yahoo Finance |
| matplotlib | Charting price, indicators, and trade results |
pip install backtesting pandas numpy pandas-ta yfinance matplotlib
Let's make sure everything is installed correctly. Save and run the code below in a Python file (e.g., test_install.py).
import backtesting import pandas as pd import numpy as np import yfinance as yf import matplotlib print("✅ All libraries imported successfully!") print("backtesting.py version:", backtesting.__version__) print("pandas version:", pd.__version__)
Sometimes, using the yfinance library may be restricted in certain regions. If you can't download data this way, you can use sample CSV files provided in our Telegram or Eita channels, or use the built-in test data from the backtesting library:
from backtesting.test import EURUSD, BTCUSD
You can also use the ccxt library for cryptocurrency data:
import ccxt exchange = ccxt.kucoin() ohlcv = exchange.fetch_ohlcv('BTC/USDT', timeframe='4h') # Convert to DataFrame with proper column names
Historical data is the fuel for our backtest engine. Without accurate, complete data, any result is meaningless.
The yfinance library provides easy access to decades of historical data for thousands of stocks and ETFs.
# File: download_data.py import yfinance as yf import pandas as pd import matplotlib.pyplot as plt # 1. Set download parameters SYMBOL = "BTC-USD" START_DATE = "2020-01-01" END_DATE = "2025-12-01" INTERVAL = "1d" print(f"Downloading data for {SYMBOL} from {START_DATE} to {END_DATE}...") # 2. Download data df = yf.download( tickers=SYMBOL, start=START_DATE, end=END_DATE, interval=INTERVAL, progress=False ) print(f"\n✅ Download complete. {len(df)} rows received.") print("Date range:", df.index[0].strftime('%Y-%m-%d'), "to", df.index[-1].strftime('%Y-%m-%d')) # 3. Save to CSV for later use csv_filename = f"{SYMBOL}_historical.csv" df.to_csv(csv_filename) print(f"\n💾 Data saved to '{csv_filename}'")
The downloaded data is a pandas DataFrame with these columns:
For backtesting, we typically use Adj Close for more realistic analysis.
Now it's time to bring our trading idea to life. We'll use the backtesting.py library, which provides a clean and efficient framework for this.
Every strategy is a Python class that inherits from the base Strategy class. It has two key methods:
# File: simple_sma_crossover.py import pandas as pd import yfinance as yf from backtesting import Backtest, Strategy from backtesting.lib import crossover class SimpleSMACrossover(Strategy): # Strategy parameters n1 = 20 n2 = 50 def init(self): close_prices = self.data.Close self.sma_short = self.I( lambda x: pd.Series(x).rolling(self.n1).mean(), close_prices, name=f'SMA{self.n1}' ) self.sma_long = self.I( lambda x: pd.Series(x).rolling(self.n2).mean(), close_prices, name=f'SMA{self.n2}' ) def next(self): # Buy signal: no position and SMA short crosses above SMA long if not self.position and crossover(self.sma_short, self.sma_long): self.buy() # Sell signal: in a position and SMA long crosses above SMA short elif self.position and crossover(self.sma_long, self.sma_short): self.position.close() if __name__ == "__main__": # Download data data = yf.download("BTC-USD", start="2020-01-01", end="2025-12-01") # Handle MultiIndex columns from yfinance if isinstance(data.columns, pd.MultiIndex): data.columns = [col[0] for col in data.columns] # Create and run backtest bt = Backtest(data, SimpleSMACrossover, cash=1000000, commission=.001, exclusive_orders=True) output = bt.run() # Display key results print("\n" + "="*50) print("📊 Backtest Results") print("="*50) print(f"Total Return: {output['Return [%]']:.2f}%") print(f"Final Equity: ${output['Equity Final [$]']:.2f}") print(f"Max Drawdown: {output['Max. Drawdown [%]']:.2f}%") print(f"Total Trades: {output['# Trades']}") print(f"Win Rate: {output['Win Rate [%]']:.2f}%") # Plot interactive charts bt.plot()

Pro Tip for Crypto: Use FractionalBacktest instead of Backtest for cryptocurrencies, as it allows fractional asset purchases.
Our simple strategy only uses SMA signals. But a professional trader always manages risk. Let's add two key rules:
# File: sma_crossover_with_sltp.py import pandas as pd from backtesting import Strategy from backtesting.lib import crossover, FractionalBacktest class SMACrossWithSLTP(Strategy): n1 = 20 n2 = 50 stop_loss_pct = 0.08 # 8% stop loss take_profit_pct = 3.5 # 350% take profit def init(self): close = self.data.Close self.sma_short = self.I(lambda x: pd.Series(x).rolling(self.n1).mean(), close) self.sma_long = self.I(lambda x: pd.Series(x).rolling(self.n2).mean(), close) def next(self): price = self.data.Close[-1] if not self.position: if crossover(self.sma_short, self.sma_long): sl = price * (1 - self.stop_loss_pct) tp = price * (1 + self.take_profit_pct) self.buy(sl=sl, tp=tp) elif self.position and crossover(self.sma_long, self.sma_short): self.position.close()
When you run the comparison, you'll typically see:
The goal is to find the right balance between return and risk.

Now let's make our strategy smarter. The RSI indicator helps us identify overbought and oversold conditions. We can use it to filter our SMA signals.
New Logic: Buy only when: 1) SMA gives a buy signal AND 2) RSI is below 30 (oversold). This means the market is correcting and a trend reversal is more likely.
# File: advanced_strategy.py import pandas as pd import numpy as np from backtesting import Backtest, Strategy from backtesting.lib import crossover def calculate_rsi(series: pd.Series, period: int = 14) -> pd.Series: delta = series.diff() gain = (delta.where(delta > 0, 0)).rolling(window=period).mean() loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean() rs = gain / loss return 100 - (100 / (1 + rs)) class AdvancedSMARSIStrategy(Strategy): sma_short = 20 sma_long = 50 rsi_period = 14 rsi_oversold = 30 rsi_overbought = 70 stop_loss = 0.06 take_profit = 0.12 def init(self): close = self.data.Close self.sma_fast = self.I(lambda x: pd.Series(x).rolling(self.sma_short).mean(), close) self.sma_slow = self.I(lambda x: pd.Series(x).rolling(self.sma_long).mean(), close) self.rsi = self.I(lambda x: calculate_rsi(pd.Series(x), self.rsi_period), close) def next(self): price = self.data.Close[-1] rsi_val = self.rsi[-1] if not np.isnan(self.rsi[-1]) else 50 if not self.position: if crossover(self.sma_fast, self.sma_slow) and rsi_val < self.rsi_oversold: sl = price * (1 - self.stop_loss) tp = price * (1 + self.take_profit) self.buy(sl=sl, tp=tp) elif self.position: if crossover(self.sma_slow, self.sma_fast) or rsi_val > self.rsi_overbought: self.position.close()
Now we have all the pieces. Let's run our final combined strategy on some data and generate a professional report.
Congratulations! You've completed this simple Python backtesting tutorial and now know how to build, test, and improve trading strategies using Python. The strategy includes moving averages, RSI filtering, stop loss, take profit, and a professional performance report.
# File: final_backtest_project.py import pandas as pd import numpy as np import yfinance as yf from datetime import datetime from backtesting import Strategy from backtesting.lib import crossover, FractionalBacktest # RSI calculation function def calculate_rsi(series, period=14): delta = series.diff() gain = (delta.where(delta > 0, 0)).rolling(window=period).mean() loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean() rs = gain / loss return 100 - (100 / (1 + rs)) # Final combined strategy class FinalCombinedStrategy(Strategy): sma_short = 20 sma_long = 60 rsi_period = 14 rsi_oversold = 35 rsi_overbought = 75 stop_loss = 0.05 take_profit = 0.15 def init(self): close = self.data.Close self.sma_fast = self.I(lambda x: pd.Series(x).rolling(self.sma_short).mean(), close) self.sma_slow = self.I(lambda x: pd.Series(x).rolling(self.sma_long).mean(), close) self.rsi = self.I(lambda x: calculate_rsi(pd.Series(x), self.rsi_period), close) def next(self): price = self.data.Close[-1] rsi_val = self.rsi[-1] if not self.position: if crossover(self.sma_fast, self.sma_slow) and rsi_val < self.rsi_oversold: sl = price*(1-self.stop_loss) tp = price*(1+self.take_profit) self.buy(sl=sl, tp=tp) elif self.position: if crossover(self.sma_slow, self.sma_fast) or rsi_val > self.rsi_overbought: self.position.close() # Main execution if __name__ == "__main__": TICKER = "BTC-USD" YEARS = 5 end_date = datetime.now().strftime("%Y-%m-%d") start_date = (datetime.now() - pd.DateOffset(years=YEARS)).strftime("%Y-%m-%d") print(f"Symbol: {TICKER}") print(f"Period: {start_date} to {end_date} ({YEARS} years)") data = yf.download(TICKER, start=start_date, end=end_date, progress=False) # Clean up columns if isinstance(data.columns, pd.MultiIndex): data.columns = [col[0] for col in data.columns] bt = FractionalBacktest(data, FinalCombinedStrategy, fractional_unit=1e-06, cash=1000, commission=.002, margin=1, exclusive_orders=True) results = bt.run() # Print comprehensive report print("\n" + "#"*70) print("Final Backtest Report") print("#"*70) print("\n1. 📊 Overall Performance") print("-"*40) print(f" Total Return: {results['Return [%]']:+.2f}%") print(f" Final Equity: ${results['Equity Final [$]']:.2f}") print("\n2. 🛡️ Risk Metrics") print("-"*40) print(f" Max Drawdown: {results['Max. Drawdown [%]']:.2f}%") print(f" Sharpe Ratio: {results.get('Sharpe Ratio', 'N/A'):.2f}") print("\n3. 🔄 Trade Statistics") print("-"*40) print(f" Total Trades: {results['# Trades']}") if results['# Trades'] > 0: print(f" Win Rate: {results['Win Rate [%]']:.1f}%") print(f" Profit Factor: {results['Profit Factor']:.2f}") bt.plot()

The Algotradium platform was designed to simplify and make accessible the backtesting and strategy-building process. It offers a web-based solution that removes many obstacles found in traditional platforms.
Simply open your web browser and go to the Algotradium platform. You do not need a broker account — just a simple registration on the platform itself.

Algotradium offers two powerful ways to define strategies:

After saving your strategy, go to the Backtests section and click "Add Backtest".
Choose your previously defined strategy
Choose market (Forex, Crypto, Gold/Silver) and specific symbol
Choose timeframe from 1 minute to 1 day
Specify exact start and end dates
Click "Create Backtest" and then "Run Backtest"

Algotradium calculates and displays a wide range of performance metrics:

Main price chart with clear markers showing entry (green arrow) and exit (red arrow) points for each trade.
Two curves on one chart: Equity Curve (account growth) and Buy & Hold Curve for easy comparison.
Bar chart showing profit/loss of each trade in chronological order — easily identify winning and losing sequences.

A unique feature of Algotradium is the ability to save every backtest result. You can store results from different tests and revisit or compare them anytime.
For deeper analysis, download complete trade reports as CSV files to open in Excel or Google Sheets for advanced statistical analysis.

Congratulations! You now have the ability to backtest a trading strategy from scratch. You've not only tested a simple strategy but also enhanced it with risk management rules and additional filters to create a more robust trading system.
In the next article, we'll cover "How to Optimize a Trading Strategy with Python". We'll learn how to automatically find the best parameters for your strategy using heatmaps and validation techniques to avoid overfitting.
Backtest results do not guarantee future profitability. Markets are dynamic and conditions change. Always:
The goal of backtesting is to increase the probability of success and reduce unnecessary risk, not to guarantee certain profits.
Want to continue learning? Read our comprehensive guide: Python Backtesting: The Complete Guide (2026) .
A simple Python backtesting tutorial teaches beginners how to test trading strategies on historical market data using Python. It explains how to install the required libraries, download market data, create trading rules, run a backtest, and evaluate the results before risking real money.
Popular Python backtesting libraries such as backtesting.py, Backtrader, and vectorbt make this process much easier by handling trade execution, portfolio management, and performance reporting automatically.
There is no single "best" Python backtesting library because the ideal choice depends on your experience and project requirements.
For most educational projects and personal strategy development, backtesting.py provides one of the best balances between simplicity, flexibility, and performance.
Yes. backtesting.py is a free and open-source Python library released under an open-source license. Anyone can install it using pip and use it for educational, research, or commercial projects.
Because it is open source, the community continuously improves the library by fixing bugs and adding new features. It also integrates well with popular Python packages such as pandas, NumPy, matplotlib, and yfinance.
Absolutely. Python is widely used for cryptocurrency strategy development and backtesting. Historical crypto data can be downloaded from services like Yahoo Finance, Binance, KuCoin, or other exchanges through APIs such as CCXT.
For cryptocurrencies, many traders use fractional position sizing because digital assets can usually be purchased in small fractions. Libraries like backtesting.py support this through FractionalBacktest, making crypto simulations more realistic.
No. Backtesting never guarantees future profitability. Historical performance only shows how a strategy would have behaved under past market conditions. Financial markets constantly evolve, meaning a profitable strategy today may stop working tomorrow.
To improve confidence in a strategy, traders should combine backtesting with out-of-sample testing, walk-forward analysis, and forward testing on live markets using small position sizes. Proper risk management remains essential regardless of historical results.
The amount of historical data depends on your trading strategy and timeframe. Swing trading strategies usually require several years of daily data, while intraday strategies may need millions of minute-level records. In general, using multiple market conditions—including bull, bear, and sideways markets—produces more reliable backtesting results.
Yes. Python backtesting works with virtually any financial market as long as historical OHLCV data is available. The same strategy framework can be adapted to stocks, ETFs, forex, cryptocurrencies, commodities, indices, and futures by simply loading the appropriate historical dataset.
Backtesting evaluates a trading strategy using historical market data, while paper trading tests the strategy in real-time market conditions without risking real money. Backtesting is faster because it simulates years of data in minutes, whereas paper trading reflects current market behavior and execution conditions.