How to build and backtest an algorithmic trading strategy in Python

August 27, 2026
Facebook logo.
Twitter logo.
LinkedIn logo.
Newsletter issue count indicatorNewsletter total issues indicator
How to build and backtest an algorithmic trading strategy in Python

How to build and backtest an algorithmic trading strategy in Python

Algorithmic trading means writing code that makes buy and sell decisions for you, based on rules you define before the market opens. Instead of watching charts and clicking buttons, your program monitors prices, checks conditions, and follows the rules exactly as written. This matters because manual trading is slow and error-prone. Code removes most of those human delays and mistakes. If you're new to the concept, the beginner's guide to algorithmic trading covers the foundations well.

The barrier to entry has dropped fast. Python, free data services, and open-source libraries like pandas mean you can build and test a strategy on your laptop. You don't need expensive terminals or dedicated servers. The 8-step framework for building algo trading strategies lays out a structured approach if you want a roadmap before writing code. The free Algorithmic Trading With Python guides show the next steps, including how to collect data automatically and send orders to a broker.

This article explains how to get price data, create a simple trading rule in Python, test it on historical prices, and judge whether the result is useful.

How Algorithmic Trading Works

An algorithmic trading system does two main things. It reads market data, then uses your rules to decide whether to send an order to a broker.

The data part is straightforward. You pull price history (or live prices) from a broker API, which is a service that lets your code request data automatically. The rules part is where your trading idea lives. Maybe you buy when a short-term average price crosses above a long-term average, and sell when it crosses below. The system then sends instructions to a broker to execute the trade.

Most beginners focus on the rules and skip everything else. That's a mistake. A strategy that looks profitable in a spreadsheet can fall apart when you account for transaction costs (the fees you pay each time you trade), delays between your decision and the actual fill, and slippage (the difference between the price you expected and the price you got). A good system handles all of this.

Why Python Dominates Algorithmic Trading

Python isn't the fastest language. C++ and Java run circles around it for raw execution speed. But speed only matters if you're competing at the microsecond level, and most individual traders are not.

Python is popular because it has a large set of libraries for data analysis and broker connections. pandas handles price data cleanly. NumPy does fast math on arrays. Libraries like yfinance pull free historical data. When you're ready to connect to a real broker, the Interactive Brokers API has solid Python wrappers.

Another advantage is that you can test an idea quickly and change the code without much overhead. That fast feedback loop matters more than execution speed for anyone still developing their approach.

Algorithmic Trading Strategy Example in Python

Let's build something concrete. A moving-average crossover strategy compares two averages of recent prices. The "fast" average uses fewer days, so it reacts quickly to price changes. The "slow" average uses more days, so it smooths out random fluctuations. When the fast average crosses above the slow one, the strategy buys. When it crosses below, the strategy sells.

This isn't a sophisticated strategy, and professionals rarely use it alone. But it's a good starting point because it teaches the basic workflow. You get price data, calculate simple measures from it, decide when to trade, and then check the results.

This example is long-only, which means it either holds the stock or holds nothing (cash). It never bets against the stock.

Getting Data and Computing Signals

import pandas as pd
import yfinance as yf

# Download daily price data for Apple
data = yf.download("AAPL", start="2020-01-01", end="2024-01-01")

# Calculate 20-day and 50-day moving averages of the closing price
data["SMA_20"] = data["Close"].rolling(window=20).mean()
data["SMA_50"] = data["Close"].rolling(window=50).mean()

# Set signal to 1 when the fast average is above the slow average, otherwise 0
data["signal"] = 0
data.loc[data["SMA_20"] > data["SMA_50"], "signal"] = 1

# The position changes one day after the signal fires
# This prevents look-ahead bias (explained below)
data["position"] = data["signal"].shift(1)

# Identify actual trades: 1 means buy, -1 means sell
data["trade"] = data["position"].diff()

# Drop rows where we don't have enough data to compute the averages
data.dropna(inplace=True)

The .shift(1) on the signal is critical. Without it, you'd be assuming you can act on today's closing price before the market closes. This mistake is called look-ahead bias. It means the code uses information that would not have been available at the time of the decision, and it makes every historical simulation look better than reality.

The trade column makes it easy to see when actual buys and sells happen. A value of 1 means the strategy entered a position, and -1 means it exited.

One note on data quality. The yfinance library is fine for learning, but free data can contain errors or adjusted-price quirks. Check it before relying on it for real money.

Measuring Performance

import numpy as np

# Calculate daily returns
data["market_return"] = data["Close"].pct_change()

# Strategy return on days we hold a position
data["strategy_return"] = data["position"] * data["market_return"]

# Subtract a small transaction cost each time the position changes
cost_per_trade = 0.001  # 0.1% round-trip cost estimate
data["strategy_return"] = data["strategy_return"] - (data["trade"].abs() * cost_per_trade)

# Cumulative returns
data["cumulative_market"] = (1 + data["market_return"]).cumprod()
data["cumulative_strategy"] = (1 + data["strategy_return"]).cumprod()

# Annualized return
# 252 is the approximate number of U.S. stock market trading days per year
# len(data) counts only rows with valid moving averages, not the full date range
total_days = len(data)
strategy_total_return = data["cumulative_strategy"].iloc[-1]
annualized_return = strategy_total_return ** (252 / total_days) - 1

# Sharpe ratio measures return compared with how unstable those returns are
# We calculate it from daily returns, then scale to an annual number
daily_mean = data["strategy_return"].mean()
daily_std = data["strategy_return"].std()
sharpe = (daily_mean / daily_std) * np.sqrt(252)

print(f"Annualized Return: {annualized_return:.2%}")
print(f"Sharpe Ratio: {sharpe:.2f}")

# Maximum drawdown shows the worst peak-to-trough decline
cumulative = data["cumulative_strategy"]
rolling_max = cumulative.cummax()
drawdown = (cumulative - rolling_max) / rolling_max
max_drawdown = drawdown.min()
print(f"Max Drawdown: {max_drawdown:.2%}")

The Sharpe ratio divides your average daily return by daily volatility (how much returns bounce around from day to day), then scales the result to a yearly number. It tells you whether you're being compensated for the risk you're taking. A Sharpe of 0.5 means modest return for the risk. Above 1.0 is solid. Below zero means you'd have been better off holding cash.

Maximum drawdown shows the largest drop from a peak value to a later low. If your strategy made 50% but had a 40% drawdown along the way, you need to ask whether you could stomach watching 40% of your gains disappear before they recovered.

Notice that the code now subtracts a small cost (0.1%) each time the position changes. This is a rough estimate, but it makes the historical simulation more honest than ignoring costs entirely.

Why Backtesting Your Algorithmic Trading Strategy Isn't Enough

A positive result on old data can still fail for predictable reasons.

One common trap is overfitting, which means you tune the rules so closely to old data that they stop working on new data. If you try 200 combinations of moving-average windows and pick the one that performed best, you've probably found a combination that matched random variation in that specific dataset. It won't repeat. A simple check is to split the data in half. Use the first half to choose your settings, then see if those same settings still work on the second half.

Another trap is survivorship bias. This means testing only on stocks that still exist today, which makes old results look better because companies that went bankrupt or got delisted disappear from the sample. If you only test on today's winners, your historical results will be inflated.

Look-ahead bias can also sneak in beyond just the .shift(1) fix. Any time your code uses information that wouldn't have been available at the moment of the trade, the results are unreliable. Common examples include using end-of-day prices to make decisions during the trading day, or including future earnings announcements in a calculation.

Transaction costs matter too. Every time the strategy switches between holding and not holding, you pay a spread (the difference between the buy and sell price a broker quotes) and possibly a commission (a flat fee per trade). A strategy that trades 300 times a year with tiny gains per trade can easily become unprofitable after costs. The guide to backtesting trading strategies with Python walks through more robust approaches.

From Backtest to Live Algorithmic Trading

The gap between a historical simulation and live trading is where most projects stall. In a backtest, your orders fill instantly at the exact price you wanted. In reality, prices move between when you decide to trade and when the order executes. Your order can also move the price if you're trading something with low volume, which means fewer shares are available at each price level.

Start small. Paper trading (simulated trading with real-time data but no real money) lets you verify that your code handles live data correctly and doesn't place duplicate orders. Most broker APIs support a paper trading mode. Run your strategy there for at least a few weeks before committing capital.

You also need logs and alerts so you can see each decision, each order, and any system failure. When something goes wrong, those logs are how you figure out what happened.

What to Build After Your First Algorithmic Trading Strategy

Use the moving-average crossover to learn the workflow, then move on to more realistic ideas. You could explore mean-reversion strategies, which bet that prices will return to an average after moving too far in one direction. Or you could try momentum strategies, which bet that recent trends will continue. The guide to quantitative trading strategies covers several of these approaches with code examples.

Most strategies follow the same workflow. Check the data, write the rule, test it on separate data, include trading costs, and run it in a simulated account before you risk real money. If you want to learn this properly, start by plotting the cumulative returns from the code above and testing the same strategy on a second stock to see if the results hold up.

Go Deeper

The free Algorithmic Trading With Python guides show the next steps, including how to collect data automatically and send orders to a broker, with worked Python examples throughout.