Algotradium

Backtesting · Python Tutorial Python Backtesting

How to Backtest a Trading Strategy in Python with backtesting.py

Simple Python backtesting tutorial example using SMA crossover

In this hands-on Python backtesting tutorial, you'll build and test a complete SMA crossover trading strategy using the backtesting.py library. Starting with historical BTC-USD data, we'll install the required Python packages, create entry and exit rules, add stop-loss and take-profit levels, and evaluate the final strategy using performance metrics.

This guide focuses on the practical process of backtesting a trading strategy in Python rather than covering every Python backtesting framework. For a broader comparison of backtesting concepts, libraries, and workflows, see our complete Python backtesting guide.

What You'll Learn in This Simple Python Backtesting Tutorial

We'll cover four main phases:

  • Setup: Installing essential Python libraries
  • Data Collection: Downloading high-quality historical price data
  • Initial Implementation: Coding a simple SMA Crossover strategy and running your first backtest
  • Enhancement: Adding Stop Loss, Take Profit, and combining with RSI for a more powerful strategy

By the end, you'll have a working Python file that can test your final strategy on any asset and produce a comprehensive report.


```html

Step 1: Set Up Python and backtesting.py

Before building our trading strategy, we need to prepare a simple Python environment. In this tutorial, we'll use backtesting.py as the backtesting framework and a few supporting Python libraries for market data, calculations, and technical indicators.

This tutorial focuses on the practical process of backtesting a trading strategy in Python. If you're looking for a broader introduction to Python backtesting, including different frameworks, backtesting concepts, performance metrics, and advanced techniques, see our complete Python backtesting guide .

Why We Use backtesting.py in This Tutorial

For this hands-on project, we'll use backtesting.py because its strategy API is simple enough to follow while still providing the essential features needed to simulate trades and evaluate a strategy.

The library lets us define a trading strategy with a Python class, calculate indicators, create entry and exit rules, execute a historical simulation, and inspect important results such as total return, drawdown, win rate, and Sharpe ratio.

Rather than comparing multiple Python backtesting frameworks here, we'll use backtesting.py throughout the tutorial so that we can concentrate on building and testing an actual trading strategy.

Install the Required Python Libraries

We only need a small set of libraries to complete this project. Each library has a specific role in the workflow:

Libraries Used in This Tutorial

LibraryPurpose in This Tutorial
backtesting.py Builds the trading strategy, simulates trades, and generates backtest results.
pandas Handles and processes historical market data.
NumPy Supports numerical calculations used by indicators and strategy logic.
yfinance Downloads historical market data for the example strategy.
pandas-ta Provides technical indicators that can be used when extending the strategy.
matplotlib Creates charts and visualizations for market and strategy analysis.

Install all required packages with:

pip install backtesting pandas numpy pandas-ta yfinance matplotlib

Verify Your Python Installation

After installing the packages, verify that Python can import the libraries correctly. Create a file named test_install.py and add the following code:

import backtesting
import pandas as pd
import numpy as np
import yfinance as yf
import matplotlib

print("All required libraries were imported successfully.")
print("backtesting.py version:", backtesting.__version__)
print("pandas version:", pd.__version__)

Run the file from your terminal:

python test_install.py

If the imports complete without errors and the package versions are displayed, your environment is ready for the next step.

What If yfinance Does Not Work?

yfinance is used in this tutorial to retrieve historical market data. Depending on your network or region, access to Yahoo Finance data may occasionally fail or return incomplete data.

If that happens, you can still continue experimenting with backtesting.py using its built-in sample datasets:

from backtesting.test import EURUSD, BTCUSD

For the main project in this tutorial, however, we'll use historical market data downloaded with yfinance.

Next: Now that our Python environment is ready, we'll download and prepare historical market data for the backtest.


```

Step 2: Collect Historical Data for Python Backtesting

Historical data is the fuel for our backtest engine. Without accurate, complete data, any result is meaningless.

Downloading Data with yfinance

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}'")

Understanding the Data Structure

The downloaded data is a pandas DataFrame with these columns:

  • Open: First price of the day
  • High: Highest price of the day
  • Low: Lowest price of the day
  • Close: Last price of the day
  • Adj Close: Adjusted close price accounting for dividends and splits
  • Volume: Number of shares traded

For backtesting, we typically use Adj Close for more realistic analysis.


Step 3: Now it's time to build the first strategy in this simple Python backtesting tutorial.

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.

Understanding the Strategy Structure in Python backtesting library backtesting.py

Every strategy is a Python class that inherits from the base Strategy class. It has two key methods:

  • init(): Defines indicators and one-time calculations
  • next(): The brain of the strategy — called for every candle (e.g., every day). Here we define our buy and sell logic.

Complete Code for the Simple SMA Crossover Strategy

# 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()
Python backtesting SMA crossover strategy results using backtesting.py
Backtest results shown in an html tab

Key Tips for Your First Backtest

  • Check the terminal output: Trade signals and dates are printed — great for debugging
  • Explore the interactive chart: View SMA lines, buy/sell arrows, equity curve, and drawdown
  • Watch Max Drawdown: A number above 20-25% is usually a warning sign

Pro Tip for Crypto: Use FractionalBacktest instead of Backtest for cryptocurrencies, as it allows fractional asset purchases.


Step 4: Upgrading with Stop Loss and Take Profit

Our simple strategy only uses SMA signals. But a professional trader always manages risk. Let's add two key rules:

  • Stop Loss: Exit with a small loss if the price moves against us
  • Take Profit: Lock in profits when the price moves in our favor

Strategy Code with SL/TP

# 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 = 0.035    # 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()

Comparing Results: Simple vs. SL/TP Strategy

When you run the comparison, you'll typically see:

  • Lower Max Drawdown: Losses are cut short
  • Higher Win Rate: More trades close profitably
  • Lower Total Return: Some big winners may be cut short — this is the price of risk management

The goal is to find the right balance between return and risk.

Comparison of a simple strategy with risk-management one
Performance comparison between simple and SL/TP strategies

Step 5: Building a Combined Advanced Strategy (SMA + RSI)

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.

Calculating RSI in backtesting.py

# 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()

Practical Project: Final Backtest with Comprehensive Report

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.

Complete Project Code

# 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()
Python backtesting final project performance report and equity curve
Complete backtest report showing all key metrics

Backtesting on the Algotradium Platform

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.

Step 1: Register and Log In

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 login page
Complete backtest report showing all key metrics

Step 2: Define Your Strategy (Strategy Builder)

Algotradium offers two powerful ways to define strategies:

Indicator-Based (No Coding)

  • Use a wide library of technical indicators (MA, RSI, MACD, etc.)
  • Fully adjustable indicator parameters
  • Create complex entry/exit conditions using operators (cross above/below, greater/less than, etc.)

Python Coding

  • For advanced users, code strategies in Python
  • Access to popular libraries like pandas-ta for technical indicators
  • Define custom indicators for unlimited creative possibilities
Algotradium strategy creation forms: no-code (left) and Python (right)
Complete backtest report showing all key metrics

Step 3: Build Your First Python Backtest Strategy

After saving your strategy, go to the Backtests section and click "Add Backtest".

1 Select Strategy

Choose your previously defined strategy

2 Select Market & Symbol

Choose market (Forex, Crypto, Gold/Silver) and specific symbol

3 Select Timeframe

Choose timeframe from 1 minute to 1 day

4 Set Date Range

Specify exact start and end dates

5 Execute Test

Click "Create Backtest" and then "Run Backtest"

Setting backtest parameters in Algotradium
Setting backtest paremeters

Advanced Analysis and Reports on Algotradium

A) Comprehensive Performance Metrics

Algotradium calculates and displays a wide range of performance metrics:

Sharpe
Sharpe Ratio
Sortino
Sortino Ratio
Drawdown
Max & Avg Drawdown
Win Rate
Win Rate
  • Sharpe & Sortino Ratios: Risk-adjusted return analysis
  • Max & Average Drawdown: Understand depth of temporary losses
  • Drawdown Duration: Time needed to recover from losses
  • Win Rate & Number of Trades: Strategy efficiency and activity
  • Total Return & Annual Return: Measure profitability
  • Buy & Hold Return: Compare your strategy to simple buy-and-hold
  • Profit Factor & Expectancy: System's inherent profitability
Complete backtest reposrt and strategy performance metrics in Algotradium
Complete backtest report showing all key metrics

B) Advanced Charts

Price Chart with Entry/Exit Markers

Main price chart with clear markers showing entry (green arrow) and exit (red arrow) points for each trade.

Equity Curve Comparison

Two curves on one chart: Equity Curve (account growth) and Buy & Hold Curve for easy comparison.

PnL Bar Chart

Bar chart showing profit/loss of each trade in chronological order — easily identify winning and losing sequences.

Return and trade charts of the backtest results in Algotradium
Return and trade charts of the backtest results in Algotradium

C) Save and Restore Results

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.

D) CSV Export

For deeper analysis, download complete trade reports as CSV files to open in Excel or Google Sheets for advanced statistical analysis.

Returns bar chart with exportable data in Algotradium
Returns bar chart with exportable data in Algotradium

Summary and Next Steps

What You've Learned

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.

What's Next?

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.

Important Final Warning

Backtest results do not guarantee future profitability. Markets are dynamic and conditions change. Always:

  • Start with capital you can afford to lose
  • Test your strategy in live markets with very small positions (Forward Test)
  • Maintain strict risk management and diversification

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) .

Frequently Asked Questions (FAQ)

What is a simple Python backtesting tutorial?

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.

Is backtesting.py free?

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.

Can I backtest cryptocurrency strategies with Python?

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.

Does backtesting guarantee future profits?

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.

How much historical data is enough for Python backtesting?

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.

Can Python backtesting be used for stocks, forex, and crypto?

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.

What is the difference between backtesting and paper trading?

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.