TraderSentiments
Quantitative Python Tutorial

Backtesting with Python: Step-by-Step Tutorial

RaptozGroupBy RaptozGroup•••16 min read

Python has established itself as the lingua franca of quantitative finance and algorithmic strategy research. Unlike rigid commercial trading software that constrains you to proprietary indicator menus, Python allows you to simulate complex mathematical models, integrate machine learning pipelines, download petabytes of free tick data, and stress-test custom risk management algorithms with complete transparency and zero licensing fees.

What You Will Master in This Tutorial

  • 1.Modern Python Libraries: Understand the strengths of Pandas, NumPy, Backtesting.py, and VectorBT for both exploratory research and rigorous event simulation.
  • 2.End-to-End Implementation: Download historical data, build a dynamic Dual Exponential Moving Average (EMA) and RSI filter strategy, and execute backtests with full trade logs.
  • 3.Real-World Market Friction: Accurately inject percentage broker commissions and slippage margins to ensure realistic P&L performance.
  • 4.Comprehensive Tearsheet: Generate complete equity curves, Sharpe ratios, Calmar ratios, and maximum drawdown profiles with interactive charts.

Executive Summary: The Python Quantitative Ecosystem

The Python quantitative landscape consists of several battle-tested libraries tailored for different computational needs. Choosing the correct library prevents architectural bottlenecks:

LibraryArchitectureSpeedLearning CurveBest Use Case
Backtesting.pyEvent-driven LightweightFast (C-optimized)Beginner friendlyRapid single-asset strategy testing & visualization
VectorBTVectorized (NumPy/Numba)Hyper-fast (Millions/sec)IntermediateMulti-asset portfolio screening & hyper-parameter grids
BacktraderEvent-driven ClassicModerateComplex OOPMulti-timeframe, multi-broker live execution
NautilusTraderRust Core / CythonUltra-low latency HFTAdvanced InstitutionalHigh-frequency tick data & order book simulation

Vectorized vs Event-Driven Python Frameworks

Before writing your first line of code, you must understand the critical trade-off between the two primary computational paradigms in Python backtesting:

Vectorized Backtesting (Pandas / VectorBT)

Vectorization processes entire matrix columns at once. For example, calculating a 50 EMA across 100,000 candles happens in 4 milliseconds using SIMD processor instructions.

Drawback: It cannot easily simulate dynamic order state transitions, such as moving a stop-loss to breakeven after price advances 1.5R, or modeling order book queue priority.

Event-Driven Backtesting (Backtesting.py / Backtrader)

The engine iterates chronologically bar-by-bar or tick-by-tick, firing events (NewBar, OrderSubmitted, OrderFilled, OrderCancelled) exactly like a real broker API.

Advantage: The logic you write in an event-driven backtest can be translated almost 1:1 into a live automated execution bot.

Step 1: Environment Setup & Historical Data Fetching

Let's set up a clean Python virtual environment and install the required quantitative dependencies: pandas, yfinance, and backtesting.

bash
# Create virtual environment and install quant stack
python -m venv quant_env
source quant_env/bin/activate  # On Windows: quant_env\Scripts\activate

pip install pandas numpy yfinance backtesting bokeh

Now, let's fetch daily price data for the S&P 500 ETF (SPY) over the past five years using yfinance. Note how we clean column headers to match Backtesting.py's required capitalized schema (Open, High, Low, Close, Volume):

python
import yfinance as yf
import pandas as pd

def fetch_data(symbol="SPY", start="2020-01-01", end="2025-12-31"):
    df = yf.download(symbol, start=start, end=end, progress=False)
    
    # Flatten MultiIndex columns if returned by yfinance
    if isinstance(df.columns, pd.MultiIndex):
        df.columns = df.columns.get_level_values(0)
        
    df = df[['Open', 'High', 'Low', 'Close', 'Volume']].dropna()
    print(f"Loaded {len(df)} bars for {symbol}")
    return df

data = fetch_data("SPY")
print(data.head())

Step 2: Defining Indicators & Strategy Logic

We will build a classic trend-following strategy: a Dual Exponential Moving Average (EMA) Crossover confirmed by an RSI Momentum Filter.

  • • Long Entry: Fast EMA (20) crosses above Slow EMA (50) AND RSI is above 50 (bullish momentum).
  • • Long Exit: Fast EMA crosses below Slow EMA OR Stop Loss (2.5%) / Take Profit (5.0%) is hit.
python
from backtesting import Strategy
from backtesting.lib import crossover

def calculate_ema(series, period):
    return pd.Series(series).ewm(span=period, adjust=False).mean()

def calculate_rsi(series, period=14):
    delta = pd.Series(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 EmaRsiStrategy(Strategy):
    fast_period = 20
    slow_period = 50
    rsi_period = 14

    def init(self):
        # Register indicators for plotting and execution
        price = self.data.Close
        self.fast_ema = self.I(calculate_ema, price, self.fast_period)
        self.slow_ema = self.I(calculate_ema, price, self.slow_period)
        self.rsi = self.I(calculate_rsi, price, self.rsi_period)

    def next(self):
        # Current bar close and indicator values
        price = self.data.Close[-1]
        
        # Entry Logic: Fast crosses above Slow with RSI > 50
        if crossover(self.fast_ema, self.slow_ema) and self.rsi[-1] > 50:
            if not self.position:
                # Place buy order with 2.5% stop loss and 5.0% take profit
                sl = price * 0.975
                tp = price * 1.05
                self.buy(sl=sl, tp=tp)
                
        # Exit Logic: Fast crosses below Slow
        elif crossover(self.slow_ema, self.fast_ema):
            if self.position.is_long:
                self.position.close()

Step 3: Modeling Realistic Fees, Commissions & Slippage

Failing to configure broker commission parameters makes any quantitative backtest worthless. In Backtesting.py, you pass a realistic percentage friction that combines commission and half-spread slippage.

Realistic Friction Benchmarks:
  • • US Equities: 0.05% to 0.10% (accounts for per-share fees and small-cap bid-ask spreads).
  • • Spot Forex: 0.02% (accounts for 1.0 pip spread on EUR/USD plus ECN commission).
  • • Crypto: 0.075% to 0.15% (maker/taker exchange fees).

Step 4: Executing the Backtest & Performance Tearsheet

With our strategy codified and realistic friction assigned, we instantiate the Backtest engine with $100,000 initial capital:

python
from backtesting import Backtest

# Instantiate backtest with $100k cash and 0.1% round-turn commission
bt = Backtest(
    data,
    EmaRsiStrategy,
    cash=100_000,
    commission=0.001,
    exclusive_orders=True
)

# Run the simulation
stats = bt.run()
print(stats)

# Generate interactive HTML tearsheet and candlestick plot
bt.plot(filename="backtest_result.html")
Example Output Tearsheet
Start 2020-01-02 00:00:00
End 2025-12-30 00:00:00
Duration 2189 days
Return [%] 142.68
Buy & Hold Return [%] 98.42
Sharpe Ratio 1.48
Sortino Ratio 2.12
Max. Drawdown [%] -14.85
Win Rate [%] 54.32
Profit Factor 1.86
# Trades 162

Step 5: Grid Search Optimization Without Overfitting

Backtesting.py features a built-in multi-parameter optimizer. However, quants must be vigilant: never optimize for raw return. Optimize for risk-adjusted stability, such as maximizing the Sharpe Ratio or Calmar Ratio:

python
# Grid Search: evaluate fast EMA from 10 to 30, slow EMA from 40 to 80
stats_opt = bt.optimize(
    fast_period=range(10, 30, 5),
    slow_period=range(40, 80, 10),
    maximize='Sharpe Ratio',
    constraint=lambda p: p.fast_period < p.slow_period
)

print(stats_opt._strategy)
print(f"Optimal Sharpe: {stats_opt['Sharpe Ratio']:.2f}")

Python vs Commercial Platforms: Decision Matrix

Should you code your backtests in Python or use commercial GUI software? Use this comparison matrix:

Choose Python If...
  • ✓You want zero recurring subscription fees and complete control over data and execution logic.
  • ✓You want to incorporate Machine Learning, Scikit-Learn, or deep statistical models.
  • ✓You need to screen and backtest thousands of assets simultaneously across global markets.
Choose GUI Software If...
  • ✓You do not want to maintain code, debug Python package updates, or configure APIs.
  • ✓You need real-time historical Level 2 DOM order book replay for futures scalping.
  • ✓You prefer visual point-and-click trade placement and instant chart scrolling.

Frequently Asked Questions

Answers to key technical questions regarding Python algorithmic backtesting, libraries, and execution accuracy.

For traders transitioning from retail charting platforms, 'Backtesting.py' is the ideal starting framework. It features an intuitive, lightweight API, automated parameter grid search, built-in Bokeh interactive HTML visualization, and requires minimal boilerplate code compared to older libraries like Backtrader.