5 Python Trading
Strategies
Five proven algorithmic trading strategies, fully implemented in Python. From beginner-friendly moving averages to advanced mean reversion. Complete with code, explanations, and backtesting.
Get the full toolkit with backtesting code
£14.99
All 5 strategies as runnable .py files with backtesting framework. One-time purchase.
Buy the Full ToolkitSecure payment via Stripe. Instant download.
Moving Average Crossover
BeginnerThe classic trend-following strategy. When a short-term moving average crosses above a long-term moving average, buy. When it crosses below, sell. Simple, effective, and the foundation of most systematic strategies.
When to Use
Best in trending markets. Performs poorly in sideways/choppy markets. Works well on daily timeframes for swing trading.
Python Implementation
import pandas as pd
import yfinance as yf
# Download data
df = yf.download("AAPL", start="2020-01-01", end="2024-01-01")
# Calculate moving averages
df["SMA_20"] = df["Close"].rolling(window=20).mean()
df["SMA_50"] = df["Close"].rolling(window=50).mean()
# Generate signals
df["signal"] = 0
df.loc[df["SMA_20"] > df["SMA_50"], "signal"] = 1 # Buy
df.loc[df["SMA_20"] < df["SMA_50"], "signal"] = -1 # Sell
# Calculate returns
df["strategy_return"] = df["signal"].shift(1) * df["Close"].pct_change()
cumulative = (1 + df["strategy_return"]).cumprod()
print(f"Total return: {(cumulative.iloc[-1] - 1) * 100:.2f}%")Key Points
- -SMA 20/50 is the most common combination for daily charts
- -EMA (exponential) reacts faster but gives more false signals
- -Add a minimum holding period to reduce whipsaws
- -Always shift signals by 1 day to avoid look-ahead bias
RSI (Relative Strength Index)
BeginnerA momentum oscillator that measures the speed and magnitude of price changes. RSI ranges from 0-100. Below 30 is considered oversold (buy signal), above 70 is overbought (sell signal). Created by J. Welles Wilder in 1978.
When to Use
Best in range-bound markets. Use as a mean-reversion signal. Combine with trend filters for higher accuracy.
Python Implementation
import pandas as pd
import yfinance as yf
df = yf.download("MSFT", start="2020-01-01", end="2024-01-01")
# Calculate RSI
delta = df["Close"].diff()
gain = delta.where(delta > 0, 0)
loss = -delta.where(delta < 0, 0)
avg_gain = gain.rolling(window=14).mean()
avg_loss = loss.rolling(window=14).mean()
rs = avg_gain / avg_loss
df["RSI"] = 100 - (100 / (1 + rs))
# Generate signals
df["signal"] = 0
df.loc[df["RSI"] < 30, "signal"] = 1 # Oversold = buy
df.loc[df["RSI"] > 70, "signal"] = -1 # Overbought = sell
# Forward fill signals (hold position until opposite signal)
df["position"] = df["signal"].replace(0, float("nan")).ffill().fillna(0)
# Calculate returns
df["strategy_return"] = df["position"].shift(1) * df["Close"].pct_change()
cumulative = (1 + df["strategy_return"]).cumprod()
print(f"Total return: {(cumulative.iloc[-1] - 1) * 100:.2f}%")Key Points
- -14-period RSI is the standard but you can adjust for sensitivity
- -RSI divergence (price makes new high but RSI does not) is a powerful signal
- -In strong trends, RSI can stay above 70 for weeks — add a trend filter
- -Combine with volume for confirmation
Bollinger Bands
IntermediateUses standard deviation bands around a moving average to identify volatility and potential reversals. Price touching the lower band suggests oversold conditions; touching the upper band suggests overbought. The bands widen in volatile markets and contract in calm ones.
When to Use
Excellent for identifying breakouts and mean-reversion trades. The 'squeeze' (narrow bands) often precedes a big move.
Python Implementation
import pandas as pd
import yfinance as yf
df = yf.download("GOOGL", start="2020-01-01", end="2024-01-01")
# Calculate Bollinger Bands
window = 20
df["SMA"] = df["Close"].rolling(window=window).mean()
df["STD"] = df["Close"].rolling(window=window).std()
df["upper_band"] = df["SMA"] + (df["STD"] * 2)
df["lower_band"] = df["SMA"] - (df["STD"] * 2)
# Bandwidth (squeeze detection)
df["bandwidth"] = (df["upper_band"] - df["lower_band"]) / df["SMA"]
# Mean reversion signals
df["signal"] = 0
df.loc[df["Close"] < df["lower_band"], "signal"] = 1 # Buy at lower band
df.loc[df["Close"] > df["upper_band"], "signal"] = -1 # Sell at upper band
df["position"] = df["signal"].replace(0, float("nan")).ffill().fillna(0)
df["strategy_return"] = df["position"].shift(1) * df["Close"].pct_change()
cumulative = (1 + df["strategy_return"]).cumprod()
print(f"Total return: {(cumulative.iloc[-1] - 1) * 100:.2f}%")
print(f"Current bandwidth: {df['bandwidth'].iloc[-1]:.4f}")Key Points
- -2 standard deviations captures ~95% of price action
- -Bollinger Squeeze: when bandwidth is at a 6-month low, expect a breakout
- -Do NOT use as a standalone system — combine with RSI or volume
- -%B indicator: (Price - Lower) / (Upper - Lower) normalises position within bands
MACD (Moving Average Convergence Divergence)
IntermediateTracks the relationship between two exponential moving averages (12 and 26 period). The MACD line minus the signal line (9 period EMA of MACD) creates a histogram. Crossovers of the MACD and signal line generate buy/sell signals.
When to Use
Best for identifying trend changes and momentum shifts. Works well on daily and weekly charts. Less effective in ranging markets.
Python Implementation
import pandas as pd
import yfinance as yf
df = yf.download("TSLA", start="2020-01-01", end="2024-01-01")
# Calculate MACD
df["EMA_12"] = df["Close"].ewm(span=12, adjust=False).mean()
df["EMA_26"] = df["Close"].ewm(span=26, adjust=False).mean()
df["MACD"] = df["EMA_12"] - df["EMA_26"]
df["signal_line"] = df["MACD"].ewm(span=9, adjust=False).mean()
df["histogram"] = df["MACD"] - df["signal_line"]
# Generate signals on crossover
df["signal"] = 0
df.loc[
(df["MACD"] > df["signal_line"]) &
(df["MACD"].shift(1) <= df["signal_line"].shift(1)),
"signal"
] = 1 # Bullish crossover
df.loc[
(df["MACD"] < df["signal_line"]) &
(df["MACD"].shift(1) >= df["signal_line"].shift(1)),
"signal"
] = -1 # Bearish crossover
df["position"] = df["signal"].replace(0, float("nan")).ffill().fillna(0)
df["strategy_return"] = df["position"].shift(1) * df["Close"].pct_change()
cumulative = (1 + df["strategy_return"]).cumprod()
print(f"Total return: {(cumulative.iloc[-1] - 1) * 100:.2f}%")Key Points
- -MACD histogram turning positive/negative often precedes the crossover
- -MACD divergence from price is one of the strongest reversal signals
- -Zero-line crossover (MACD crossing above/below 0) confirms trend direction
- -Combine with volume: crossovers on high volume are more reliable
Mean Reversion
AdvancedBased on the statistical principle that prices tend to return to their mean over time. When price deviates significantly from its average (measured by z-score), trade in the direction of the mean. This is the foundation of pairs trading and statistical arbitrage.
When to Use
Best for stocks with high mean-reversion tendencies (utilities, ETFs). Avoid in trending stocks. Works well on intraday and daily timeframes.
Python Implementation
import pandas as pd
import numpy as np
import yfinance as yf
df = yf.download("SPY", start="2020-01-01", end="2024-01-01")
# Calculate z-score
lookback = 30
df["mean"] = df["Close"].rolling(window=lookback).mean()
df["std"] = df["Close"].rolling(window=lookback).std()
df["z_score"] = (df["Close"] - df["mean"]) / df["std"]
# Entry and exit thresholds
entry_threshold = 2.0
exit_threshold = 0.0
df["signal"] = 0
df.loc[df["z_score"] < -entry_threshold, "signal"] = 1 # Buy when very oversold
df.loc[df["z_score"] > entry_threshold, "signal"] = -1 # Short when very overbought
df.loc[abs(df["z_score"]) < exit_threshold, "signal"] = 0 # Exit at mean
df["position"] = df["signal"].replace(0, float("nan")).ffill().fillna(0)
df["strategy_return"] = df["position"].shift(1) * df["Close"].pct_change()
# Risk metrics
returns = df["strategy_return"].dropna()
sharpe = np.sqrt(252) * returns.mean() / returns.std()
max_dd = (returns.cumsum() - returns.cumsum().cummax()).min()
cumulative = (1 + returns).cumprod()
print(f"Total return: {(cumulative.iloc[-1] - 1) * 100:.2f}%")
print(f"Sharpe ratio: {sharpe:.2f}")
print(f"Max drawdown: {max_dd * 100:.2f}%")Key Points
- -Z-score of 2 means price is 2 standard deviations from the mean — statistically extreme
- -Half-life of mean reversion tells you how fast a stock reverts (use Ornstein-Uhlenbeck)
- -Always set stop losses — mean reversion fails spectacularly in regime changes
- -Pairs trading applies this concept: find two correlated stocks and trade the spread
Strategy Comparison
| Strategy | Type | Best Market | Difficulty |
|---|---|---|---|
| MA Crossover | Trend following | Trending | Beginner |
| RSI | Momentum | Range-bound | Beginner |
| Bollinger Bands | Volatility | Any | Intermediate |
| MACD | Trend + Momentum | Trending | Intermediate |
| Mean Reversion | Statistical | Range-bound | Advanced |
Disclaimer: This content is for educational purposes only and does not constitute financial advice. Past performance does not indicate future results. Algorithmic trading involves significant risk of loss. Always paper trade first and never risk money you cannot afford to lose.
Get the Full Toolkit
All 5 strategies as clean, runnable Python files. Includes a backtesting framework, performance metrics, and a README with setup instructions.
£14.99
Buy Now — £14.99Secure checkout via Stripe. Instant download of all code.