Cryptocurrency trading has evolved rapidly, and traders are increasingly turning to data-driven strategies to gain an edge in volatile markets. One powerful way to evaluate a trading idea is through backtesting—simulating a strategy on historical data to assess its performance. In this guide, we’ll walk through how to backtest a popular trading strategy combining the Stochastic oscillator, Relative Strength Index (RSI), and Moving Average Convergence Divergence (MACD) using Python.
This multi-indicator approach aims to improve signal accuracy by combining momentum, trend confirmation, and overbought/oversold detection—reducing false signals that often plague single-indicator systems.
Understanding the Indicators
Before diving into code, let’s break down how each indicator contributes to the strategy.
Stochastic Oscillator: Detecting Overbought and Oversold Levels
The Stochastic oscillator compares a cryptocurrency’s closing price to its price range over a specific period. It consists of two lines: %K (the fast line) and %D (the slow, signal line).
- When both %K and %D rise above 75, the asset is considered overbought.
- When both fall below 25, it's considered oversold.
👉 Discover how technical indicators can boost your trading precision.
However, relying solely on Stochastic can lead to premature entries during strong trends. That’s why we pair it with other confirmatory tools.
RSI: Confirming the Trend Direction
While many use RSI as another overbought/oversold tool, in this strategy, we repurpose it for trend confirmation.
- If RSI moves above 50, it suggests bullish momentum—supporting a buy signal.
- If RSI drops below 50, bearish momentum is likely—favoring a sell.
This subtle shift turns RSI from a standalone signal generator into a trend filter, improving trade quality.
MACD: Validating Momentum
The MACD measures the relationship between two moving averages and helps detect shifts in momentum.
Instead of acting on every MACD crossover (which often fails in sideways markets), we only consider signals when they align with Stochastic and RSI conditions.
This layered confirmation reduces noise and increases confidence in each trade.
Building the Strategy in Python
Let’s implement this step by step using widely available libraries.
Install and Import Required Libraries
import pandas as pd
import numpy as np
import yfinance as yf
import matplotlib.pyplot as pltYou can install them via pip:
pip install pandas numpy yfinance matplotlibFetch Cryptocurrency Price Data
We'll use yfinance to pull BTC-USD data at 30-minute intervals:
ticker = "BTC-USD"
data = yf.download(ticker, period="60d", interval="30m")
df = pd.DataFrame(data)You can replace "BTC-USD" with "ETH-USD" or "BNB-USD" for other coins.
Calculate Technical Indicators
1. Stochastic Oscillator
def calculate_stochastic(df, k_period=14, d_period=3):
low_min = df['Low'].rolling(window=k_period).min()
high_max = df['High'].rolling(window=k_period).max()
df['%K'] = 100 * (df['Close'] - low_min) / (high_max - low_min)
df['%D'] = df['%K'].rolling(window=d_period).mean()
return df2. RSI
def calculate_rsi(df, period=14):
delta = df['Close'].diff()
gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
rs = gain / loss
df['RSI'] = 100 - (100 / (1 + rs))
return df3. MACD
def calculate_macd(df, fast=12, slow=26, signal=9):
exp1 = df['Close'].ewm(span=fast).mean()
exp2 = df['Close'].ewm(span=slow).mean()
df['MACD'] = exp1 - exp2
df['Signal_Line'] = df['MACD'].ewm(span=signal).mean()
return dfApply all functions:
df = calculate_stochastic(df)
df = calculate_rsi(df)
df = calculate_macd(df)Generate Buy and Sell Signals
We’ll define conditions based on all three indicators:
Buy Signal:
- Stochastic (%K and %D) < 25 (oversold)
- RSI > 50 (bullish trend)
- MACD crosses above Signal Line
Sell Signal:
- Stochastic (%K and %D) > 75 (overbought)
- RSI < 50 (bearish trend)
- MACD crosses below Signal Line
To avoid whipsaws, we’ll add a lag check—ensuring conditions were met within the last few candles.
def generate_signals(df, lags=4):
# Stochastic buy/sell conditions within lag window
df['Stoch_Oversold'] = np.where((df['%K'] < 25) & (df['%D'] < 25), 1, 0)
df['Stoch_Overbought'] = np.where((df['%K'] > 75) & (df['%D'] > 75), 1, 0)
# Rolling check over 'lags' periods
df['Buy_Conf'] = df['Stoch_Oversold'].rolling(lags).sum()
df['Sell_Conf'] = df['Stoch_Overbought'].rolling(lags).sum()
# RSI trend filter
df['RSI_Buy'] = np.where(df['RSI'] > 50, 1, 0)
df['RSI_Sell'] = np.where(df['RSI'] < 50, 1, 0)
# MACD crossover
df['MACD_Buy'] = np.where(df['MACD'] > df['Signal_Line'], 1, 0)
df['MACD_Sell'] = np.where(df['MACD'] < df['Signal_Line'], 1, 0)
# Final signals
df['Buy_Signal'] = np.where(
(df['Buy_Conf'] > 0) &
(df['RSI_Buy'] == 1) &
(df['MACD_Buy'] == 1), 1, 0)
df['Sell_Signal'] = np.where(
(df['Sell_Conf'] > 0) &
(df['RSI_Sell'] == 1) &
(df['MACD_Sell'] == 1), 1, 0)
return dfExecute Backtest Logic
Now extract valid trade entries and exits:
Buying_dates = []
Selling_dates = []
for i in range(len(df)):
if df['Buy_Signal'][i] and len(Selling_dates) == len(Buying_dates):
Buying_dates.append(df.index[i])
if df['Sell_Signal'][i] and len(Buying_dates) > len(Selling_dates):
Selling_dates.append(df.index[i])Remove overlapping trades:
frame = pd.DataFrame({'Buying_dates': Buying_dates, 'Selling_dates': Selling_dates})
actuals = frame[frame.Buying_dates > frame.Selling_dates.shift(1)].copy()Calculate returns:
def calculate_returns(trades, df):
profits = []
for _, row in trades.iterrows():
buy_price = df.loc[row.Buying_dates, 'Open']
sell_price = df.loc[row.Selling_dates, 'Open']
returns = (sell_price - buy_price) / buy_price * 100
profits.append(returns)
return np.mean(profits), np.sum(profits)
avg_return, total_return = calculate_returns(actuals, df)
print(f"Mean Return per Trade: {avg_return:.2f}%")
print(f"Cumulative Return: {total_return:.2f}%")Visualize the Results
plt.figure(figsize=(20,10))
plt.plot(df.index, df['Close'], color='black', alpha=0.7, label='BTC Price')
plt.scatter(actuals.Buying_dates, df.loc[actuals.Buying_dates]['Open'],
marker='^', color='green', s=500, label='Buy Signal')
plt.scatter(actuals.Selling_dates, df.loc[actuals.Selling_dates]['Open'],
marker='v', color='red', s=500, label='Sell Signal')
plt.title('BTC-USD: Stochastic + RSI + MACD Strategy Signals')
plt.legend()
plt.show()Performance Across Major Cryptocurrencies
| Asset | Trades | Avg Return | Cumulative Return |
|---|---|---|---|
| BTC-USD | 6 | 2.43% | 15.39% |
| ETH-USD | 5 | 1.16% | 5.90% |
| BNB-USD | 6 | 1.34% | 7.30% |
👉 See how algorithmic strategies perform in real-time markets.
Results show consistent profitability across top-tier assets during trending markets.
Frequently Asked Questions (FAQ)
Q: Can this strategy work on lower timeframes like 5-minute charts?
A: Yes, but increased volatility may generate more false signals. Proper risk management and additional filters are recommended.
Q: Why use RSI above/below 50 instead of traditional overbought/oversold levels?
A: Because we already have the Stochastic for overbought/oversold detection. Using RSI as a trend filter avoids redundancy and improves signal quality.
Q: What causes overlapping buy/sell signals?
A: Overlapping occurs when multiple buy signals trigger before a prior sell. We resolve this by ensuring each new buy follows a completed sell.
Q: Is this strategy suitable for low-volume altcoins?
A: Not recommended. Low liquidity leads to wide bid-ask spreads and slippage, which can erase small gains from short-term trades.
Q: How can I optimize the parameters?
A: Experiment with different lags, indicator periods (e.g., RSI length), and timeframes. Use walk-forward analysis to avoid overfitting.
Q: Can I automate this strategy?
A: Absolutely. Once validated, you can integrate it with trading APIs like OKX or Binance using Python scripts or bots.
Limitations and Considerations
While results are promising, several limitations exist:
- Market Conditions Matter: The strategy performs best in trending markets. In sideways or choppy conditions, indicators may generate conflicting signals.
- Timeframe Sensitivity: We used 30-minute candles, but shorter intervals (e.g., 5 or 15 minutes) might yield different outcomes due to noise.
- No Transaction Costs Included: Real-world trading involves fees and slippage—always factor these into your backtests.
- No Stop-Loss Mechanism: This version doesn’t include risk controls. Adding stop-loss/take-profit rules can improve risk-adjusted returns.
Final Thoughts
Combining Stochastic, RSI, and MACD creates a robust multi-layered trading system that leverages the strengths of each indicator while mitigating individual weaknesses. Backtesting in Python allows you to validate ideas quickly and objectively.
Whether you're exploring automated trading or refining manual strategies, this framework offers a solid foundation for further development.
👉 Start applying your backtested strategies on a reliable trading platform today.