Backtesting a trading strategy in Python with yfinance

francocos1 pts0 comments

How to Backtest a Trading Strategy in Python — NWCastSkip to contentAboutPrivacyTerms

NWCast

Home  / Investing & Markets<br>Investing & Markets<br>How to Backtest a Trading Strategy in Python<br>Backtesting a trading strategy in Python means running your trading rules against historical price data to see how they would have performed. You can do th<br>By Franco CosmelliJuly 6, 2026July 6, 20268 min read<br>Updated July 6, 2026

```html

Most retail trading strategies fail because nobody tested them before risking real money. Backtesting a trading strategy in Python means running your trading rules against historical price data to measure what would have happened — total return, maximum drawdown, win rate — using pandas for calculation and yfinance for free stock data. You can build a complete backtest in 30 minutes. What you learn may save you years of losses.

Key Takeaways

Use yfinance to download historical price data and pandas to calculate moving averages and generate buy/sell signals

A complete backtest tracks each trade's entry, exit, and return, then calculates portfolio-level metrics including total return and drawdown

Backtesting reveals whether a strategy had historical edge, but does not guarantee future performance — overfitting and slippage destroy most strategies in live trading

Difficulty: Intermediate<br>Time needed: 30–40 minutes<br>For: readers who understand moving averages and Python basics, want to test strategies before risking capital

Before You Start

This guide assumes you know what a moving average crossover strategy is: when a short-term moving average crosses above a long-term moving average, you buy. When it crosses below, you sell. You should be comfortable running Python scripts, installing libraries, and reading a DataFrame.

You do not need prior experience with finance libraries. But understand this: backtesting historical data is not the same as trading live markets. Slippage, commissions, and liquidity constraints are not modeled here. A strategy that shows 18% annual returns in a backtest may lose money the first month you trade it.

What You Need

Python 3.8 or later installed on your system

pandas library — install with pip install pandas

yfinance library — install with pip install yfinance

A code editor or Jupyter Notebook to write and run your script

No API keys or paid accounts required — yfinance pulls free data from Yahoo Finance

Step 1: Download Historical Price Data

Open your Python environment and import the libraries. Use yfinance to download daily closing prices for a stock. For this example, we'll use $SPY (S&P 500 ETF) from January 1, 2020 to December 31, 2023 . Four years of data. Bull market, COVID crash, recovery, Fed tightening.

Run this code:

import yfinance as yf<br>import pandas as pd

ticker = "SPY"<br>data = yf.download(ticker, start="2020-01-01", end="2023-12-31")<br>data = data[['Close']].copy()<br>data.columns = ['price']<br>print(data.head())

You should see a DataFrame with dates as the index and a price column showing SPY's closing price each day. If the download fails, check your internet connection and verify the ticker symbol is correct.

Step 2: Calculate Moving Averages

Add two moving averages to your DataFrame: a 20-day short-term average and a 50-day long-term average. These are common parameters for a simple crossover strategy. Pandas makes this straightforward with the .rolling() method.

data['ma_short'] = data['price'].rolling(window=20).mean()<br>data['ma_long'] = data['price'].rolling(window=50).mean()<br>data = data.dropna()<br>print(data.tail())

The dropna() removes the first 50 rows where the long moving average cannot be calculated. You need complete data for both indicators before generating signals. This is not optional.

Step 3: Generate Buy and Sell Signals

Create a signal column where 1 means "hold long" and 0 means "hold cash." The signal turns to 1 when the short MA crosses above the long MA. Turns to 0 when it crosses below. Detect crossovers by comparing today's MA positions to yesterday's.

data['signal'] = 0<br>data['signal'] = (data['ma_short'] > data['ma_long']).astype(int)<br>data['position'] = data['signal'].diff()<br>print(data[data['position'] != 0])

The position column shows 1 for buy signals and -1 for sell signals. Print the rows where position is non-zero. Those are your trade dates.

Photo by Brecht Corbeel / Unsplash

Step 4: Calculate Trade-Level Returns

Now calculate the return for each trade. Start by calculating daily returns, then multiply by your signal to get strategy returns. When the signal is 1 , you capture the day's return. When it's 0 , you earn zero.

data['market_return'] = data['price'].pct_change()<br>data['strategy_return'] = data['market_return'] * data['signal'].shift(1)<br>data = data.dropna()<br>print(data[['price', 'signal', 'strategy_return']].head(10))

The .shift(1) is critical. It ensures you enter trades based on yesterday's signal, not today's price. This prevents look-ahead bias — where your backtest "knows"...

data price trading strategy signal python

Related Articles