🇪🇸 Leer en Español 🇺🇸 English

F4: Your First Complete Strategy

Fundamental Module 4 - Duration: 2-3 hours

Module Objectives

After completing this module you will have:

  • A complete and functional trading strategy
  • Clear entry, exit, and risk management rules
  • A backtest that proves your strategy with real data
  • Metrics to evaluate whether your strategy is profitable

The Strategy: “Golden Cross with Filters”

We will build a real and proven strategy step by step.

Why This Strategy?

  • Simple but effective: Easy to understand and program
  • Historically proven: Used by institutional funds
  • Adaptable: Works in different markets
  • Great for learning: Includes all essential components

Strategy Components

Component Description
Main Signal Golden Cross (SMA 50 crosses SMA 200)
Filter 1 RSI not overbought (< 70)
Filter 2 Volume > 20-day average
Stop Loss 5% from entry
Take Profit 15% from entry
Risk Management Maximum 2% of capital per trade

Step 1: Build the Strategy

Strategy Base Code

# For Google Colab
!pip install yfinance pandas numpy matplotlib

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

class GoldenCrossStrategy:
    """
    Golden Cross Strategy with Quality Filters
    """

    def __init__(self, symbol, start_date, end_date, initial_capital=10000):
        self.symbol = symbol
        self.start_date = start_date
        self.end_date = end_date
        self.initial_capital = initial_capital
        self.capital = initial_capital

        # Strategy parameters
        self.short_sma = 50
        self.long_sma = 200
        self.rsi_period = 14
        self.rsi_overbought = 70
        self.volume_filter = 1.2  # 20% above average
        self.stop_loss_pct = 0.05   # 5%
        self.take_profit_pct = 0.15 # 15%
        self.risk_per_trade = 0.02 # 2% of capital

        # Tracking data
        self.trades = []
        self.current_position = None

    def download_data(self):
        """Downloads and prepares the data"""
        print(f"Downloading data for {self.symbol}...")

        # Download with extra margin to calculate indicators
        extended_start = pd.to_datetime(self.start_date) - timedelta(days=300)
        self.data = yf.download(self.symbol, start=extended_start, end=self.end_date)

        if self.data.empty:
            raise ValueError(f"No data found for {self.symbol}")

        print(f"Downloaded {len(self.data)} days of data")

    def calculate_indicators(self):
        """Calculates all required indicators"""
        print("Calculating indicators...")

        # Moving averages
        self.data['SMA_50'] = self.data['Close'].rolling(window=self.short_sma).mean()
        self.data['SMA_200'] = self.data['Close'].rolling(window=self.long_sma).mean()

        # RSI
        delta = self.data['Close'].diff()
        gain = (delta.where(delta > 0, 0)).rolling(window=self.rsi_period).mean()
        loss = (-delta.where(delta < 0, 0)).rolling(window=self.rsi_period).mean()
        rs = gain / loss
        self.data['RSI'] = 100 - (100 / (1 + rs))

        # Average volume
        self.data['Volume_MA'] = self.data['Volume'].rolling(window=20).mean()
        self.data['Volume_Ratio'] = self.data['Volume'] / self.data['Volume_MA']

        # Crossover signals
        self.data['Golden_Cross'] = (
            (self.data['SMA_50'] > self.data['SMA_200']) &
            (self.data['SMA_50'].shift(1) <= self.data['SMA_200'].shift(1))
        )

        self.data['Death_Cross'] = (
            (self.data['SMA_50'] < self.data['SMA_200']) &
            (self.data['SMA_50'].shift(1) >= self.data['SMA_200'].shift(1))
        )

        # Clean NaN from beginning
        self.data = self.data[self.data.index >= self.start_date].dropna()

        print("Indicators calculated")

    def generate_signals(self):
        """Generates buy/sell signals based on the strategy"""
        print("Generating trading signals...")

        self.data['Signal'] = 0  # 0: No position, 1: Buy, -1: Sell

        for i in range(len(self.data)):
            date = self.data.index[i]
            row = self.data.iloc[i]

            # BUY SIGNAL
            if (row['Golden_Cross'] and
                row['RSI'] < self.rsi_overbought and
                row['Volume_Ratio'] > self.volume_filter and
                self.current_position is None):

                self.data.loc[date, 'Signal'] = 1
                self.open_position(date, row['Close'], 'LONG')

            # CHECK STOPS IF POSITION EXISTS
            elif self.current_position is not None:
                current_price = row['Close']
                entry_price = self.current_position['entry_price']

                # Stop Loss
                if current_price <= entry_price * (1 - self.stop_loss_pct):
                    self.data.loc[date, 'Signal'] = -1
                    self.close_position(date, current_price, 'STOP_LOSS')

                # Take Profit
                elif current_price >= entry_price * (1 + self.take_profit_pct):
                    self.data.loc[date, 'Signal'] = -1
                    self.close_position(date, current_price, 'TAKE_PROFIT')

                # Death Cross (signal exit)
                elif row['Death_Cross']:
                    self.data.loc[date, 'Signal'] = -1
                    self.close_position(date, current_price, 'DEATH_CROSS')

        # Close position at end if still open
        if self.current_position is not None:
            last_price = self.data['Close'].iloc[-1]
            self.close_position(self.data.index[-1], last_price, 'END_OF_PERIOD')

        print(f"Generated {len(self.trades)} trades")

    def open_position(self, date, price, type):
        """Opens a new position"""

        # Calculate position size based on risk
        capital_at_risk = self.capital * self.risk_per_trade
        stop_loss_price = price * (1 - self.stop_loss_pct)
        risk_per_share = price - stop_loss_price
        num_shares = int(capital_at_risk / risk_per_share)

        # Limit to available capital
        max_shares = int(self.capital * 0.95 / price)  # Use max 95% of capital
        num_shares = min(num_shares, max_shares)

        self.current_position = {
            'entry_date': date,
            'entry_price': price,
            'num_shares': num_shares,
            'type': type,
            'stop_loss': stop_loss_price,
            'take_profit': price * (1 + self.take_profit_pct)
        }

    def close_position(self, date, price, reason):
        """Closes the current position and records the trade"""

        if self.current_position is None:
            return

        # Calculate result
        entry_price = self.current_position['entry_price']
        num_shares = self.current_position['num_shares']
        profit_loss = (price - entry_price) * num_shares
        return_pct = ((price - entry_price) / entry_price) * 100

        # Update capital
        self.capital += profit_loss

        # Record trade
        trade = {
            'entry_date': self.current_position['entry_date'],
            'exit_date': date,
            'entry_price': entry_price,
            'exit_price': price,
            'num_shares': num_shares,
            'profit_loss': profit_loss,
            'return_pct': return_pct,
            'exit_reason': reason,
            'capital_after': self.capital
        }
        self.trades.append(trade)

        # Clear position
        self.current_position = None

    def calculate_metrics(self):
        """Calculates strategy performance metrics"""

        if not self.trades:
            print("No trades to analyze")
            return None

        df_trades = pd.DataFrame(self.trades)

        # Basic metrics
        total_trades = len(df_trades)
        winning_trades = len(df_trades[df_trades['profit_loss'] > 0])
        losing_trades = len(df_trades[df_trades['profit_loss'] < 0])
        win_rate = (winning_trades / total_trades) * 100

        # Gains and losses
        total_gains = df_trades[df_trades['profit_loss'] > 0]['profit_loss'].sum()
        total_losses = abs(df_trades[df_trades['profit_loss'] < 0]['profit_loss'].sum())
        profit_factor = total_gains / total_losses if total_losses > 0 else np.inf

        # Returns
        total_return = ((self.capital - self.initial_capital) / self.initial_capital) * 100
        avg_win = df_trades[df_trades['profit_loss'] > 0]['return_pct'].mean()
        avg_loss = df_trades[df_trades['profit_loss'] < 0]['return_pct'].mean()

        # Maximum drawdown
        peak_capital = self.initial_capital
        max_drawdown = 0
        for trade in self.trades:
            peak_capital = max(peak_capital, trade['capital_after'])
            drawdown = ((peak_capital - trade['capital_after']) / peak_capital) * 100
            max_drawdown = max(max_drawdown, drawdown)

        # Buy & Hold comparison
        initial_price = self.data['Close'].iloc[0]
        final_price = self.data['Close'].iloc[-1]
        buy_hold_return = ((final_price - initial_price) / initial_price) * 100

        metrics = {
            'total_trades': total_trades,
            'winning_trades': winning_trades,
            'losing_trades': losing_trades,
            'win_rate': win_rate,
            'profit_factor': profit_factor,
            'total_return': total_return,
            'avg_win': avg_win,
            'avg_loss': avg_loss,
            'max_drawdown': max_drawdown,
            'final_capital': self.capital,
            'buy_hold_return': buy_hold_return
        }

        return metrics

    def visualize_results(self):
        """Creates result visualizations"""

        fig, axes = plt.subplots(4, 1, figsize=(15, 16), sharex=True)

        # 1. Price and Signals
        ax1 = axes[0]
        ax1.plot(self.data.index, self.data['Close'], label='Price', linewidth=1, alpha=0.7)
        ax1.plot(self.data.index, self.data['SMA_50'], label='SMA 50', linewidth=1, alpha=0.7)
        ax1.plot(self.data.index, self.data['SMA_200'], label='SMA 200', linewidth=1, alpha=0.7)

        # Mark entries and exits
        entries = self.data[self.data['Signal'] == 1]
        exits = self.data[self.data['Signal'] == -1]

        ax1.scatter(entries.index, entries['Close'], color='green', marker='^',
                   s=100, label=f'Buys ({len(entries)})', zorder=5)
        ax1.scatter(exits.index, exits['Close'], color='red', marker='v',
                   s=100, label=f'Sells ({len(exits)})', zorder=5)

        ax1.set_title(f'{self.symbol} - Golden Cross Strategy with Filters', fontsize=14)
        ax1.set_ylabel('Price ($)')
        ax1.legend(loc='upper left')
        ax1.grid(True, alpha=0.3)

        # 2. RSI
        ax2 = axes[1]
        ax2.plot(self.data.index, self.data['RSI'], color='purple', linewidth=1)
        ax2.axhline(y=70, color='red', linestyle='--', alpha=0.5)
        ax2.axhline(y=30, color='green', linestyle='--', alpha=0.5)
        ax2.set_ylabel('RSI')
        ax2.set_ylim(0, 100)
        ax2.grid(True, alpha=0.3)

        # 3. Volume
        ax3 = axes[2]
        colors = ['green' if s == 1 else 'red' if s == -1 else 'gray' for s in self.data['Signal']]
        ax3.bar(self.data.index, self.data['Volume'], color=colors, alpha=0.5)
        ax3.plot(self.data.index, self.data['Volume_MA'], color='blue', linewidth=1)
        ax3.set_ylabel('Volume')
        ax3.grid(True, alpha=0.3)

        # 4. Equity Curve
        ax4 = axes[3]
        if self.trades:
            equity_data = [self.initial_capital]
            equity_dates = [self.data.index[0]]

            for trade in self.trades:
                equity_dates.append(trade['exit_date'])
                equity_data.append(trade['capital_after'])

            ax4.plot(equity_dates, equity_data, color='blue', linewidth=2, label='Strategy')

            # Compare with Buy & Hold
            buy_hold_values = self.initial_capital * (self.data['Close'] / self.data['Close'].iloc[0])
            ax4.plot(self.data.index, buy_hold_values, color='gray', linewidth=1,
                    alpha=0.7, label='Buy & Hold')

            ax4.set_ylabel('Capital ($)')
            ax4.set_xlabel('Date')
            ax4.legend()
            ax4.grid(True, alpha=0.3)

        plt.tight_layout()
        plt.show()

    def run_backtest(self):
        """Runs the complete backtest"""
        print("\n" + "="*60)
        print(f"BACKTESTING: {self.symbol}")
        print(f"Period: {self.start_date} to {self.end_date}")
        print(f"Initial Capital: ${self.initial_capital:,.2f}")
        print("="*60)

        # Run steps
        self.download_data()
        self.calculate_indicators()
        self.generate_signals()

        # Calculate metrics
        metrics = self.calculate_metrics()

        if metrics:
            print("\nBACKTEST RESULTS:")
            print("="*60)
            print(f"Total Trades: {metrics['total_trades']}")
            print(f"Winning Trades: {metrics['winning_trades']}")
            print(f"Losing Trades: {metrics['losing_trades']}")
            print(f"\nWin Rate: {metrics['win_rate']:.2f}%")
            print(f"Profit Factor: {metrics['profit_factor']:.2f}")
            print(f"\nFinal Capital: ${metrics['final_capital']:,.2f}")
            print(f"Total Return: {metrics['total_return']:.2f}%")
            print(f"Max Drawdown: {metrics['max_drawdown']:.2f}%")
            print(f"\nBuy & Hold Return: {metrics['buy_hold_return']:.2f}%")

            if metrics['total_return'] > metrics['buy_hold_return']:
                print("\nThe strategy BEATS Buy & Hold!")
            else:
                print("\nThe strategy DOES NOT beat Buy & Hold")

        # Visualize
        self.visualize_results()

        return metrics

# RUN THE STRATEGY
if __name__ == "__main__":
    # Configure parameters
    SYMBOL = 'AAPL'  # You can change the symbol
    START_DATE = '2020-01-01'
    END_DATE = '2024-01-01'
    INITIAL_CAPITAL = 10000

    # Create and run strategy
    strategy = GoldenCrossStrategy(SYMBOL, START_DATE, END_DATE, INITIAL_CAPITAL)
    results = strategy.run_backtest()

Step 2: Optimize the Strategy

Testing Different Parameters

def optimize_parameters(symbol='AAPL', start_date='2020-01-01', end_date='2024-01-01'):
    """Tests different parameter combinations"""

    print("PARAMETER OPTIMIZATION")
    print("="*60)

    optimization_results = []

    # Parameters to test
    short_smas = [20, 30, 50]
    long_smas = [100, 150, 200]
    stop_losses = [0.03, 0.05, 0.07]
    take_profits = [0.10, 0.15, 0.20]

    best_result = None
    best_return = -np.inf

    for sma_s in short_smas:
        for sma_l in long_smas:
            if sma_s >= sma_l:
                continue

            for sl in stop_losses:
                for tp in take_profits:
                    # Create strategy with these parameters
                    strategy = GoldenCrossStrategy(symbol, start_date, end_date)
                    strategy.short_sma = sma_s
                    strategy.long_sma = sma_l
                    strategy.stop_loss_pct = sl
                    strategy.take_profit_pct = tp

                    # Run backtest silently
                    try:
                        strategy.download_data()
                        strategy.calculate_indicators()
                        strategy.generate_signals()
                        metrics = strategy.calculate_metrics()

                        if metrics and metrics['total_trades'] > 0:
                            result = {
                                'short_sma': sma_s,
                                'long_sma': sma_l,
                                'stop_loss': sl,
                                'take_profit': tp,
                                'return': metrics['total_return'],
                                'win_rate': metrics['win_rate'],
                                'profit_factor': metrics['profit_factor'],
                                'max_drawdown': metrics['max_drawdown'],
                                'num_trades': metrics['total_trades']
                            }

                            optimization_results.append(result)

                            if metrics['total_return'] > best_return:
                                best_return = metrics['total_return']
                                best_result = result

                    except:
                        continue

    # Show results
    if best_result:
        print("\nBEST PARAMETERS FOUND:")
        print("="*60)
        print(f"Short SMA: {best_result['short_sma']}")
        print(f"Long SMA: {best_result['long_sma']}")
        print(f"Stop Loss: {best_result['stop_loss']*100:.1f}%")
        print(f"Take Profit: {best_result['take_profit']*100:.1f}%")
        print(f"\nReturn: {best_result['return']:.2f}%")
        print(f"Win Rate: {best_result['win_rate']:.2f}%")
        print(f"Profit Factor: {best_result['profit_factor']:.2f}")
        print(f"Max Drawdown: {best_result['max_drawdown']:.2f}%")

    return pd.DataFrame(optimization_results)

# Run optimization
df_optimization = optimize_parameters('AAPL')

# View top 5 combinations
print("\nTOP 5 COMBINATIONS:")
print(df_optimization.nlargest(5, 'return')[['short_sma', 'long_sma', 'stop_loss', 'take_profit', 'return', 'win_rate']])

Step 3: Validation and Improvements

Analyzing Weaknesses

def detailed_trade_analysis(strategy):
    """Detailed analysis of each trade"""

    if not strategy.trades:
        print("No trades to analyze")
        return

    df_trades = pd.DataFrame(strategy.trades)

    print("\nDETAILED TRADE ANALYSIS:")
    print("="*60)

    # Analysis by exit reason
    print("\nExit Distribution:")
    for reason in df_trades['exit_reason'].unique():
        reason_trades = df_trades[df_trades['exit_reason'] == reason]
        avg_return = reason_trades['return_pct'].mean()
        count = len(reason_trades)
        print(f"{reason}: {count} trades, Average return: {avg_return:.2f}%")

    # Trade duration
    df_trades['duration'] = (pd.to_datetime(df_trades['exit_date']) -
                            pd.to_datetime(df_trades['entry_date'])).dt.days

    print(f"\nAverage duration: {df_trades['duration'].mean():.1f} days")
    print(f"Maximum duration: {df_trades['duration'].max()} days")
    print(f"Minimum duration: {df_trades['duration'].min()} days")

    # Best and worst trade
    best_trade = df_trades.loc[df_trades['profit_loss'].idxmax()]
    worst_trade = df_trades.loc[df_trades['profit_loss'].idxmin()]

    print(f"\nBest Trade:")
    print(f"  Entry: {best_trade['entry_date']}")
    print(f"  Gain: ${best_trade['profit_loss']:.2f} ({best_trade['return_pct']:.2f}%)")

    print(f"\nWorst Trade:")
    print(f"  Entry: {worst_trade['entry_date']}")
    print(f"  Loss: ${worst_trade['profit_loss']:.2f} ({worst_trade['return_pct']:.2f}%)")

    return df_trades

# Analyze trades from the last strategy
df_trades = detailed_trade_analysis(strategy)

Final Project: Multi-Strategy

Comparing Multiple Strategies

def compare_strategies():
    """Compares different strategies over the same period"""

    strategy_configs = [
        {'name': 'Golden Cross Original', 'sma_s': 50, 'sma_l': 200, 'sl': 0.05, 'tp': 0.15},
        {'name': 'Golden Cross Aggressive', 'sma_s': 20, 'sma_l': 50, 'sl': 0.03, 'tp': 0.10},
        {'name': 'Golden Cross Conservative', 'sma_s': 50, 'sma_l': 200, 'sl': 0.07, 'tp': 0.20},
        {'name': 'Golden Cross Fast', 'sma_s': 10, 'sma_l': 30, 'sl': 0.02, 'tp': 0.05},
    ]

    comparison_results = []

    for config in strategy_configs:
        print(f"\nTesting: {config['name']}...")

        strategy = GoldenCrossStrategy('SPY', '2020-01-01', '2024-01-01', 10000)
        strategy.short_sma = config['sma_s']
        strategy.long_sma = config['sma_l']
        strategy.stop_loss_pct = config['sl']
        strategy.take_profit_pct = config['tp']

        strategy.download_data()
        strategy.calculate_indicators()
        strategy.generate_signals()
        metrics = strategy.calculate_metrics()

        if metrics:
            comparison_results.append({
                'Strategy': config['name'],
                'Return (%)': metrics['total_return'],
                'Win Rate (%)': metrics['win_rate'],
                'Profit Factor': metrics['profit_factor'],
                'Max DD (%)': metrics['max_drawdown'],
                'Trades': metrics['total_trades']
            })

    # Create comparison table
    df_comparison = pd.DataFrame(comparison_results)
    df_comparison = df_comparison.round(2)

    print("\n" + "="*80)
    print("STRATEGY COMPARISON")
    print("="*80)
    print(df_comparison.to_string(index=False))

    # Identify winner
    best_strategy = df_comparison.loc[df_comparison['Return (%)'].idxmax()]
    print(f"\nBEST STRATEGY: {best_strategy['Strategy']}")
    print(f"   Return: {best_strategy['Return (%)']}%")
    print(f"   Profit Factor: {best_strategy['Profit Factor']}")

    return df_comparison

# Run comparison
df_comparison = compare_strategies()

Module Checkpoint

Complete Strategy

  • My strategy has clear entry rules
  • I implemented stop loss and take profit
  • I use risk management (2% per trade)
  • I combine multiple indicators

Working Backtest

  • I can test with real historical data
  • I calculate performance metrics
  • I compare with Buy & Hold
  • I visualize results clearly

Optimization

  • I tested different parameters
  • I identified best configurations
  • I analyzed individual trades
  • I compared multiple variations

Learnings

  • I understand why some strategies fail
  • I know the importance of stop loss
  • I see the impact of parameters
  • I can improve the strategy

CONGRATULATIONS!

You have completed the FUNDAMENTALS of quantitative trading.

You now have:

  • Knowledge of what being a quant means
  • Python trading skills
  • Mastery of technical indicators
  • Your first complete and tested strategy

Next Steps

Continue with STRATEGIES (Level 2)

E1: Momentum Trading

  • Gap & Go for small caps
  • Breakout strategies
  • Momentum scanning

E2: Mean Reversion

  • VWAP reclaim
  • Oversold bounces
  • Pairs trading

E3: Advanced Backtesting

  • Walk-forward analysis
  • Monte Carlo simulation
  • Stress testing

Final Reflection

“A mediocre strategy well executed is better than a perfect strategy poorly executed.”

Your first strategy won’t be perfect, but you now have the tools to continuously improve it. Every day of trading generates new data to learn from.

You are officially a Quant Trader in training!


Ready for more advanced strategies? -> E1: Momentum Trading