In partnership with

13 Investment Errors You Should Avoid

Successful investing is often less about making the right moves and more about avoiding the wrong ones. With our guide, 13 Retirement Investment Blunders to Avoid, you can learn ways to steer clear of common errors to help get the most from your $1M+ portfolio—and enjoy the retirement you deserve.

🚀 Your Investing Journey Just Got Better: Premium Subscriptions Are Here! 🚀

It’s been 4 months since we launched our premium subscription plans at GuruFinance Insights, and the results have been phenomenal! Now, we’re making it even better for you to take your investing game to the next level. Whether you’re just starting out or you’re a seasoned trader, our updated plans are designed to give you the tools, insights, and support you need to succeed.

Here’s what you’ll get as a premium member:

  • Exclusive Trading Strategies: Unlock proven methods to maximize your returns.

  • In-Depth Research Analysis: Stay ahead with insights from the latest market trends.

  • Ad-Free Experience: Focus on what matters most—your investments.

  • Monthly AMA Sessions: Get your questions answered by top industry experts.

  • Coding Tutorials: Learn how to automate your trading strategies like a pro.

  • Masterclasses & One-on-One Consultations: Elevate your skills with personalized guidance.

Our three tailored plans—Starter Investor, Pro Trader, and Elite Investor—are designed to fit your unique needs and goals. Whether you’re looking for foundational tools or advanced strategies, we’ve got you covered.

Don’t wait any longer to transform your investment strategy. The last 4 months have shown just how powerful these tools can be—now it’s your turn to experience the difference.

This article examines a robust Python framework for backtesting RSI (Relative Strength Index) trading strategies, specifically designed to eliminate look-ahead bias and provide institutional-grade performance analysis. The framework demonstrates best practices in quantitative finance by implementing proper temporal separation, realistic signal execution, and comprehensive performance measurement.

It’s go-time for holiday campaigns

Roku Ads Manager makes it easy to extend your Q4 campaign to performance CTV.

You can:

  • Easily launch self-serve CTV ads

  • Repurpose your social content for TV

  • Drive purchases directly on-screen with shoppable ads

  • A/B test to discover your most effective offers

The holidays only come once a year. Get started now with a $500 ad credit when you spend your first $500 today with code: ROKUADS500. Terms apply.

Framework Architecture and Design Philosophy

The framework employs a modular architecture with three core components: signal generation, strategy execution, and performance analysis. The design prioritizes statistical rigor over complexity, ensuring that backtesting results reflect realistic trading conditions rather than optimistic theoretical scenarios.

Key Design Principles:

  • Temporal integrity: Signals generated on day T can only be executed on day T+1

  • Realistic execution: All trades occur at market close with proper timing delays

  • Comprehensive validation: Both in-sample optimization and out-of-sample testing

  • Statistical robustness: Multiple performance metrics beyond simple returns

RSI Calculation and Signal Generation

Technical Implementation

The framework calculates RSI using a rolling window approach rather than the exponential smoothing method, providing more intuitive parameter interpretation:

def calculate_rsi(prices, period=14):
    delta = prices.diff()
    gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
    loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
    rs = gain / loss
    rsi = 100 - (100 / (1 + rs))
    return rsi

This implementation uses simple moving averages for gain and loss calculations, making the RSI more responsive to recent price changes while maintaining the indicator’s fundamental mean-reversion characteristics.

Signal Logic and Bias Prevention

The strategy employs classical RSI thresholds:

  • Buy signals: RSI < 30 (oversold condition)

  • Sell signals: RSI > 70 (overbought condition)

Critical bias prevention mechanism:

for i in range(1, len(df)):
    prev_signal = df['signal'].iloc[i-1]  # Use previous day's signal
    
    if prev_signal == 1 and position == 0:  # Buy
        position = 1.0
        df.loc[df.index[i], 'trade'] = 1

This ensures that signals generated at market close on day T are executed at market close on day T+1, eliminating the impossible scenario of acting on same-day information.

200+ AI Side Hustles to Start Right Now

From prompt engineering to AI apps, there are countless ways to profit from AI now. Our guide reveals 200+ actionable AI business models, from no-code solutions to advanced applications. Learn how people are earning $500-$10,000 monthly with tools that didn't exist last year. Sign up for The Hustle to get the guide and daily insights.

Backtesting Methodology and Validation Framework

Temporal Data Splitting

The framework implements proper temporal separation with clean cutoff dates:

  • In-sample period: 2017–2019 (strategy development and parameter selection)

  • Out-of-sample period: 2020–2022 (unbiased performance validation)

This 60/40 split provides sufficient data for both optimization and validation while maintaining chronological integrity essential for time-series analysis.

Performance Metrics Calculation

The framework calculates institutional-grade performance metrics:

Risk-Adjusted Returns:

  • Sharpe Ratio: Annualized excess return per unit of volatility

  • Maximum Drawdown: Worst peak-to-trough decline

  • Volatility: Annualized standard deviation of returns

Trading Efficiency:

  • Win Rate: Percentage of profitable trading periods

  • Total Return: Cumulative strategy performance

  • Annualized Return: Geometric mean return scaled to annual basis

Buy-and-Hold Benchmark

Each backtest includes comprehensive benchmarking against passive investment, providing essential context for strategy evaluation. The framework calculates identical metrics for both active strategy and buy-and-hold approaches, enabling direct performance comparison.

Results Analysis and Interpretation

Strategy Performance Characteristics

The RSI mean-reversion strategy typically exhibits:

Favorable Conditions:

  • Volatile sideways markets: RSI excels when prices oscillate within ranges

  • Market corrections: Oversold conditions often provide profitable entry points

  • High-volatility environments: Increased RSI signal frequency enhances opportunity capture

Challenging Conditions:

  • Strong trending markets: Mean-reversion signals work against momentum

  • Low volatility periods: Reduced signal generation limits strategy activity

  • Regime changes: Parameter optimization may not transfer across market cycles

Overfitting Detection

The framework includes overfitting assessment through performance consistency analysis:

if in_ret > 0 and out_ret <= 0:
    print("In-sample profitable, out-of-sample unprofitable (potential overfitting)")

This automated check identifies the classic overfitting pattern where in-sample optimization produces positive results that fail to generalize to new data.

Visualization and Trade Analysis

Comprehensive Visualization Suite

The framework generates four-panel analytical displays:

  1. Price charts with trade markers: Visual confirmation of signal timing

  2. RSI indicator plots: Signal generation context and threshold behaviour

  3. Cumulative return comparison: Strategy vs. benchmark performance

  4. Separate in-sample/out-of-sample analysis: Temporal validation integrity

Trade Execution Analysis

Detailed trade statistics provide operational insights:

  • Signal frequency: Buy/sell signal counts per period

  • Round-trip completion: Matched entry/exit trade pairs

  • Execution timing: Visual confirmation of bias-free signal implementation

Framework Strengths and Limitations

Notable Strengths

Methodological Rigor: Proper temporal separation and bias prevention meet institutional standards for strategy validation.

Comprehensive Analysis: Multiple performance metrics and visualization tools provide complete strategy assessment.

Practical Implementation: Realistic signal timing and execution constraints reflect actual trading conditions.

Automation Features: Overfitting detection and consistency analysis reduce manual interpretation requirements.

Inherent Limitations

Transaction Cost Absence: Framework doesn’t incorporate bid-ask spreads, commissions, or market impact costs.

Single Asset Focus: Portfolio-level analysis and diversification effects remain unaddressed.

Static Parameters: RSI periods and thresholds don’t adapt to changing market conditions.

Market Regime Independence: Strategy assumes consistent mean-reversion behaviour across all market environments.

Here is the full implementation:

import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime

def calculate_rsi(prices, period=14):
    """Calculate RSI (Relative Strength Index)"""
    delta = prices.diff()
    gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
    loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
    rs = gain / loss
    rsi = 100 - (100 / (1 + rs))
    return rsi

def backtest_rsi_strategy(data, rsi_oversold=30, rsi_overbought=70):
    """
    RSI Strategy without look-ahead bias:
    - Generate signals based on previous day's RSI
    - Buy when RSI < oversold (30)
    - Sell when RSI > overbought (70)
    """
    df = data.copy()
    df['position'] = 0.0
    df['signal'] = 0
    df['trade'] = 0
    
    # Generate signals (no look-ahead bias)
    df.loc[df['RSI'] < rsi_oversold, 'signal'] = 1  # Buy signal
    df.loc[df['RSI'] > rsi_overbought, 'signal'] = -1  # Sell signal
    
    # Execute trades the day AFTER signal (prevents look-ahead bias)
    position = 0.0
    positions = np.zeros(len(df))
    
    for i in range(1, len(df)):
        prev_signal = df['signal'].iloc[i-1]  # Use previous day's signal
        
        if prev_signal == 1 and position == 0:  # Buy
            position = 1.0
            df.loc[df.index[i], 'trade'] = 1
        elif prev_signal == -1 and position == 1:  # Sell
            position = 0.0
            df.loc[df.index[i], 'trade'] = -1
            
        positions[i] = position
    
    df['position'] = positions
    
    # Calculate returns
    df['returns'] = df['Close'].pct_change()
    df['strategy_returns'] = df['position'].shift(1) * df['returns']
    
    # Fix NaN values
    df['strategy_returns'] = df['strategy_returns'].fillna(0)
    df['returns'] = df['returns'].fillna(0)
    
    df['cumulative_returns'] = (1 + df['returns']).cumprod()
    df['cumulative_strategy'] = (1 + df['strategy_returns']).cumprod()
    
    return df

def calculate_metrics(returns):
    """Calculate performance metrics"""
    returns_clean = returns.dropna()
    if len(returns_clean) == 0:
        return {}
        
    total_return = (1 + returns_clean).prod() - 1
    annualized_return = (1 + total_return) ** (252 / len(returns_clean)) - 1
    volatility = returns_clean.std() * np.sqrt(252)
    sharpe_ratio = annualized_return / volatility if volatility != 0 else 0
    
    # Maximum drawdown
    cumulative = (1 + returns_clean).cumprod()
    running_max = cumulative.expanding().max()
    drawdown = (cumulative - running_max) / running_max
    max_drawdown = drawdown.min()
    
    # Win rate
    positive_returns = returns_clean[returns_clean > 0]
    win_rate = len(positive_returns) / len(returns_clean) if len(returns_clean) > 0 else 0
    
    return {
        'Total Return': f"{total_return:.2%}",
        'Annualized Return': f"{annualized_return:.2%}",
        'Volatility': f"{volatility:.2%}",
        'Sharpe Ratio': f"{sharpe_ratio:.3f}",
        'Max Drawdown': f"{max_drawdown:.2%}",
        'Win Rate': f"{win_rate:.2%}"
    }

# Download AAPL data
print("Downloading AAPL data...")
ticker = "AAPL"
start_date = "2016-12-01"  # Start earlier to have enough data for RSI calculation
end_date = "2022-12-31"

data = yf.download(ticker, start=start_date, end=end_date)

# Calculate RSI
data['RSI'] = calculate_rsi(data['Close'])

# Remove initial rows with NaN RSI values
data = data.dropna()

# Split into periods (ensure clean split)
split_date = '2020-01-01'
in_sample = data[data.index < split_date].copy()
out_sample = data[data.index >= split_date].copy()

print(f"\nIn-Sample Period: {in_sample.index[0].date()} to {in_sample.index[-1].date()}")
print(f"Out-of-Sample Period: {out_sample.index[0].date()} to {out_sample.index[-1].date()}")

# In-Sample Backtest (2017-2019)
print("\n" + "="*50)
print("IN-SAMPLE BACKTEST")
print("="*50)

in_sample_results = backtest_rsi_strategy(in_sample)

# In-Sample Metrics
buy_hold_returns_in = in_sample_results['returns'].dropna()
strategy_returns_in = in_sample_results['strategy_returns'].dropna()

print("\nBuy & Hold Metrics (In-Sample):")
buy_hold_metrics_in = calculate_metrics(buy_hold_returns_in)
for key, value in buy_hold_metrics_in.items():
    print(f"{key}: {value}")

print("\nRSI Strategy Metrics (In-Sample):")
strategy_metrics_in = calculate_metrics(strategy_returns_in)
for key, value in strategy_metrics_in.items():
    print(f"{key}: {value}")

# Out-of-Sample Backtest (2020-2022)
print("\n" + "="*50)
print("OUT-OF-SAMPLE BACKTEST")
print("="*50)

out_sample_results = backtest_rsi_strategy(out_sample)

# Out-of-Sample Metrics
buy_hold_returns_out = out_sample_results['returns'].dropna()
strategy_returns_out = out_sample_results['strategy_returns'].dropna()

print("\nBuy & Hold Metrics (Out-of-Sample):")
buy_hold_metrics_out = calculate_metrics(buy_hold_returns_out)
for key, value in buy_hold_metrics_out.items():
    print(f"{key}: {value}")

print("\nRSI Strategy Metrics (Out-of-Sample):")
strategy_metrics_out = calculate_metrics(strategy_returns_out)
for key, value in strategy_metrics_out.items():
    print(f"{key}: {value}")

# Visualization
fig, axes = plt.subplots(2, 2, figsize=(15, 12))
fig.suptitle('RSI Strategy Backtest - AAPL (No Look-Ahead Bias)', fontsize=16)

# In-Sample: Price and RSI
ax1 = axes[0, 0]
ax1.plot(in_sample_results.index, in_sample_results['Close'], label='AAPL Price', color='blue')
ax1.set_title('In-Sample: AAPL Price')
ax1.set_ylabel('Price ($)')
ax1.legend(loc='upper left')

# Add buy/sell markers
buy_signals = in_sample_results[in_sample_results['trade'] == 1]
sell_signals = in_sample_results[in_sample_results['trade'] == -1]
ax1.scatter(buy_signals.index, buy_signals['Close'], color='green', marker='^', s=50, label='Buy')
ax1.scatter(sell_signals.index, sell_signals['Close'], color='red', marker='v', s=50, label='Sell')

ax1_rsi = ax1.twinx()
ax1_rsi.plot(in_sample_results.index, in_sample_results['RSI'], label='RSI', color='orange', alpha=0.7)
ax1_rsi.axhline(y=70, color='r', linestyle='--', alpha=0.7, label='Overbought')
ax1_rsi.axhline(y=30, color='g', linestyle='--', alpha=0.7, label='Oversold')
ax1_rsi.set_ylabel('RSI')
ax1_rsi.set_ylim(0, 100)
ax1_rsi.legend(loc='upper right')

# In-Sample: Cumulative Returns
axes[0, 1].plot(in_sample_results.index, in_sample_results['cumulative_returns'], 
                label='Buy & Hold', color='blue', linewidth=2)
axes[0, 1].plot(in_sample_results.index, in_sample_results['cumulative_strategy'], 
                label='RSI Strategy', color='red', linewidth=2)
axes[0, 1].set_title('In-Sample: Cumulative Returns')
axes[0, 1].set_ylabel('Cumulative Returns')
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)

# Out-of-Sample: Price and RSI
ax3 = axes[1, 0]
ax3.plot(out_sample_results.index, out_sample_results['Close'], label='AAPL Price', color='blue')
ax3.set_title('Out-of-Sample: AAPL Price')
ax3.set_ylabel('Price ($)')
ax3.legend(loc='upper left')

# Add buy/sell markers
buy_signals_out = out_sample_results[out_sample_results['trade'] == 1]
sell_signals_out = out_sample_results[out_sample_results['trade'] == -1]
ax3.scatter(buy_signals_out.index, buy_signals_out['Close'], color='green', marker='^', s=50, label='Buy')
ax3.scatter(sell_signals_out.index, sell_signals_out['Close'], color='red', marker='v', s=50, label='Sell')

ax3_rsi = ax3.twinx()
ax3_rsi.plot(out_sample_results.index, out_sample_results['RSI'], label='RSI', color='orange', alpha=0.7)
ax3_rsi.axhline(y=70, color='r', linestyle='--', alpha=0.7, label='Overbought')
ax3_rsi.axhline(y=30, color='g', linestyle='--', alpha=0.7, label='Oversold')
ax3_rsi.set_ylabel('RSI')
ax3_rsi.set_ylim(0, 100)
ax3_rsi.legend(loc='upper right')

# Out-of-Sample: Cumulative Returns
axes[1, 1].plot(out_sample_results.index, out_sample_results['cumulative_returns'], 
                label='Buy & Hold', color='blue', linewidth=2)
axes[1, 1].plot(out_sample_results.index, out_sample_results['cumulative_strategy'], 
                label='RSI Strategy', color='red', linewidth=2)
axes[1, 1].set_title('Out-of-Sample: Cumulative Returns')
axes[1, 1].set_ylabel('Cumulative Returns')
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# Trade Analysis
print("\n" + "="*60)
print("TRADE ANALYSIS")
print("="*60)

# In-Sample trades
buy_trades_in = (in_sample_results['trade'] == 1).sum()
sell_trades_in = (in_sample_results['trade'] == -1).sum()

print(f"\nIn-Sample Trades:")
print(f"Buy signals: {buy_trades_in}")
print(f"Sell signals: {sell_trades_in}")
print(f"Complete round trips: {min(buy_trades_in, sell_trades_in)}")

# Out-of-Sample trades
buy_trades_out = (out_sample_results['trade'] == 1).sum()
sell_trades_out = (out_sample_results['trade'] == -1).sum()

print(f"\nOut-of-Sample Trades:")
print(f"Buy signals: {buy_trades_out}")
print(f"Sell signals: {sell_trades_out}")
print(f"Complete round trips: {min(buy_trades_out, sell_trades_out)}")

# Final Summary
print("\n" + "="*60)
print("SUMMARY COMPARISON")
print("="*60)
print("\nIN-SAMPLE:")
print(f"Buy & Hold Return: {buy_hold_metrics_in.get('Total Return', 'N/A')}")
print(f"RSI Strategy Return: {strategy_metrics_in.get('Total Return', 'N/A')}")
print(f"RSI Sharpe Ratio: {strategy_metrics_in.get('Sharpe Ratio', 'N/A')}")

print("\nOUT-OF-SAMPLE:")
print(f"Buy & Hold Return: {buy_hold_metrics_out.get('Total Return', 'N/A')}")
print(f"RSI Strategy Return: {strategy_metrics_out.get('Total Return', 'N/A')}")
print(f"RSI Sharpe Ratio: {strategy_metrics_out.get('Sharpe Ratio', 'N/A')}")

print(f"\nStrategy Consistency: ", end="")
try:
    in_ret = float(strategy_metrics_in.get('Total Return', '0%').replace('%', ''))
    out_ret = float(strategy_metrics_out.get('Total Return', '0%').replace('%', ''))
    if in_ret > 0 and out_ret > 0:
        print("Both periods profitable")
    elif in_ret > 0 and out_ret <= 0:
        print("In-sample profitable, out-of-sample unprofitable (potential overfitting)")
    elif in_ret <= 0 and out_ret > 0:
        print("In-sample unprofitable, out-of-sample profitable")
    else:
        print("Both periods unprofitable")
except:
    print("Unable to assess consistency")
Buy & Hold Metrics (In-Sample):
Total Return: 162.85%
Annualized Return: 37.66%
Volatility: 24.54%
Sharpe Ratio: 1.534
Max Drawdown: -38.52%
Win Rate: 54.72%

RSI Strategy Metrics (In-Sample):
Total Return: 24.06%
Annualized Return: 7.39%
Volatility: 14.63%
Sharpe Ratio: 0.505
Max Drawdown: -26.53%
Win Rate: 12.73%

==================================================
OUT-OF-SAMPLE BACKTEST
==================================================

Buy & Hold Metrics (Out-of-Sample):
Total Return: 76.63%
Annualized Return: 20.88%
Volatility: 36.91%
Sharpe Ratio: 0.566
Max Drawdown: -31.43%
Win Rate: 51.32%

RSI Strategy Metrics (Out-of-Sample):
Total Return: -0.36%
Annualized Return: -0.12%
Volatility: 29.45%
Sharpe Ratio: -0.004
Max Drawdown: -29.19%
Win Rate: 22.22%
============================================================
TRADE ANALYSIS
============================================================

In-Sample Trades:
Buy signals: 8
Sell signals: 8
Complete round trips: 8

Out-of-Sample Trades:
Buy signals: 8
Sell signals: 7
Complete round trips: 7

============================================================
SUMMARY COMPARISON
============================================================

IN-SAMPLE:
Buy & Hold Return: 162.85%
RSI Strategy Return: 24.06%
RSI Sharpe Ratio: 0.505

OUT-OF-SAMPLE:
Buy & Hold Return: 76.63%
RSI Strategy Return: -0.36%
RSI Sharpe Ratio: -0.004

Strategy Consistency: In-sample profitable, out-of-sample unprofitable (potential overfitting)

Keep Reading

No posts found