← Back to Home
Trading Breakouts from Consolidation with Momentum Ignition Strategy - Optimization and Rolling Backtest

Trading Breakouts from Consolidation with Momentum Ignition Strategy - Optimization and Rolling Backtest

The Momentum Ignition Strategy is designed to identify and capitalize on strong directional moves that emerge after periods of price consolidation. The core idea is that tight price ranges often precede significant breakouts. By combining volatility analysis for consolidation detection with Rate of Change (ROC) for momentum ignition, and filtering with a long-term trend, the strategy aims to enter trades at the cusp of a new trend. All positions are managed with a dynamic ATR-based trailing stop.

Strategy Overview

The MomentumIgnitionStrategy defines trade entries and exits based on several key components:

Entry Logic: An entry is triggered when the following conditions align, indicating a potential strong directional move:

  1. Consolidation Detection: The strategy first identifies periods of low volatility, where price is consolidating. This is determined by comparing the standard deviation of the closing price over a consolidation_period to the current close price. If this ratio falls below a consolidation_threshold, the market is considered to be consolidating.
  2. Momentum Ignition: Once consolidation is detected, the strategy looks for a sudden surge in momentum that breaks out of the ROC’s recent range. The ROC (Rate of Change) is calculated, and its moving average and standard deviation are used to define a channel.
    • Bullish Ignition: The current ROC value rises above its ROC moving average plus a multiple (roc_breakout_std) of its ROC standard deviation.
    • Bearish Ignition: The current ROC value falls below its ROC moving average minus a multiple (roc_breakout_std) of its ROC standard deviation.
  3. Trend Filter: To align trades with the broader market direction, a long-term Simple Moving Average (SMA) acts as a trend filter.
    • For long entries, the current close price must be above the trend_sma.
    • For short entries, the current close price must be below the trend_sma.

All three sets of conditions (consolidation, momentum ignition, and trend alignment) must be met for an order to be placed.

Exit Logic (ATR Trailing Stop): Once a position is open, a dynamic ATR-based trailing stop is employed for risk management and profit protection. This type of stop automatically adjusts as the price moves in the favor of the trade.

Backtrader Implementation

Here’s the MomentumIgnitionStrategy class:

import backtrader as bt
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import dateutil.relativedelta as rd
import warnings
from scipy import stats # Not directly used in the strategy logic provided, but in overall script
import seaborn as sns # Not directly used in the strategy logic provided, but in overall script
import multiprocessing # Not directly used in the strategy logic provided, but in overall script

warnings.filterwarnings("ignore")

class MomentumIgnitionStrategy(bt.Strategy):
    params = (
        ('consolidation_period', 30),
        ('consolidation_threshold', 0.05),
        ('roc_period', 7),
        ('roc_ma_period', 30),
        ('roc_breakout_std', 1.0),
        ('trend_period', 30),
        ('atr_period', 7),
        ('atr_stop_multiplier', 3.0),
        ('order_percentage', 0.95),
        ('printlog', False),
    )

    def __init__(self):
        self.order = None
        self.price_stddev = bt.indicators.StandardDeviation(self.data.close, period=self.p.consolidation_period)
        self.roc = bt.indicators.RateOfChange(self.data.close, period=self.p.roc_period)
        self.roc_ma = bt.indicators.SimpleMovingAverage(self.roc, period=self.p.roc_ma_period)
        self.roc_stddev = bt.indicators.StandardDeviation(self.roc, period=self.p.roc_ma_period)
        self.trend_sma = bt.indicators.SimpleMovingAverage(self.data.close, period=self.p.trend_period)
        self.atr = bt.indicators.AverageTrueRange(self.data, period=self.p.atr_period)
        
        self.stop_price = None
        self.highest_price_since_entry = None
        self.lowest_price_since_entry = None
        
        self.trade_count = 0
        self.long_entries = 0
        self.short_entries = 0
        self.atr_stop_exits = 0
        self.profitable_trades = 0

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]: 
            return
        if order.status in [order.Completed]:
            if order.isbuy():
                self.trade_count += 1
                self.long_entries += 1
                if self.position: # Position is now open
                    self.highest_price_since_entry = self.data.high[0]
                    self.stop_price = self.highest_price_since_entry - (self.atr[0] * self.p.atr_stop_multiplier)
            elif order.issell():
                self.trade_count += 1
                self.short_entries += 1
                if self.position: # Position is now open
                    self.lowest_price_since_entry = self.data.low[0]
                    self.stop_price = self.lowest_price_since_entry + (self.atr[0] * self.p.atr_stop_multiplier)
            if not self.position: # Position was closed
                self.stop_price = None
                self.highest_price_since_entry = None
                self.lowest_price_since_entry = None
        self.order = None

    def notify_trade(self, trade):
        if not trade.isclosed: 
            return
        if trade.pnl > 0:
            self.profitable_trades += 1

    def next(self):
        if self.order: 
            return

        if not self.position:
            # Entry Logic
            is_consolidating = (self.price_stddev[0] / self.data.close[0]) < self.p.consolidation_threshold
            is_macro_uptrend = self.data.close[0] > self.trend_sma[0]
            is_macro_downtrend = self.data.close[0] < self.trend_sma[0]

            if is_consolidating:
                roc_upper_band = self.roc_ma[0] + (self.roc_stddev[0] * self.p.roc_breakout_std)
                roc_lower_band = self.roc_ma[0] - (self.roc_stddev[0] * self.p.roc_breakout_std)
                
                is_mom_breakout_up = self.roc[0] > roc_upper_band
                is_mom_breakout_down = self.roc[0] < roc_lower_band
                
                if is_macro_uptrend and is_mom_breakout_up:
                    cash = self.broker.get_cash()
                    size = (cash * self.p.order_percentage) / self.data.close[0]
                    self.order = self.buy(size=size)
                elif is_macro_downtrend and is_mom_breakout_down:
                    cash = self.broker.get_cash()
                    size = (cash * self.p.order_percentage) / self.data.close[0]
                    self.order = self.sell(size=size)

        elif self.position:
            # Trailing Stop Exit Logic
            if self.position.size > 0: # Long position
                self.highest_price_since_entry = max(self.highest_price_since_entry, self.data.high[0])
                new_stop = self.highest_price_since_entry - (self.atr[0] * self.p.atr_stop_multiplier)
                self.stop_price = max(self.stop_price, new_stop) # Only move stop up
                if self.data.close[0] < self.stop_price: 
                    self.order = self.close()
                    self.atr_stop_exits += 1
            elif self.position.size < 0: # Short position
                self.lowest_price_since_entry = min(self.lowest_price_since_entry, self.data.low[0])
                new_stop = self.lowest_price_since_entry + (self.atr[0] * self.p.atr_stop_multiplier)
                self.stop_price = min(self.stop_price, new_stop) # Only move stop down
                if self.data.close[0] > self.stop_price: 
                    self.order = self.close()
                    self.atr_stop_exits += 1

Parameters (params):

Initialization (__init__):

Order and Trade Notifications (notify_order, notify_trade):

Main Logic (next):

The next method contains the core trading logic, executed on each new bar of data:

  1. Pending Order Check: if self.order: return ensures no new orders are placed while one is pending.
  2. Entry Logic (if not self.position):
    • Consolidation Check: is_consolidating is true if the normalized price standard deviation is below consolidation_threshold.
    • Macro Trend Check: is_macro_uptrend (close > trend_sma) or is_macro_downtrend (close < trend_sma).
    • Momentum Breakout Check: If consolidating, it defines roc_upper_band and roc_lower_band based on roc_ma and roc_stddev. is_mom_breakout_up is true if roc[0] crosses above roc_upper_band; is_mom_breakout_down is true if roc[0] crosses below roc_lower_band.
    • Order Placement: If is_macro_uptrend AND is_mom_breakout_up, a buy order is placed. If is_macro_downtrend AND is_mom_breakout_down, a sell order is placed. Position size is determined by order_percentage.
  3. Exit Logic (elif self.position - ATR Trailing Stop):
    • Long Position: self.highest_price_since_entry is updated. A new_stop is calculated using atr_stop_multiplier. self.stop_price is updated to always move in the profitable direction (max for long). If data.close[0] falls below self.stop_price, the position is closed, and self.atr_stop_exits is incremented.
    • Short Position: self.lowest_price_since_entry is updated. A new_stop is calculated. self.stop_price is updated to always move in the profitable direction (min for short). If data.close[0] rises above self.stop_price, the position is closed, and self.atr_stop_exits is incremented.

Comprehensive Analysis Framework

The following code provides a complete framework for analyzing the MomentumIgnitionStrategy, including optimization and a rolling backtest.

def run_momentum_analysis():
    # OPTIMIZATION
    print("="*60)
    print("MOMENTUM IGNITION STRATEGY OPTIMIZATION")
    print("="*60)

    df = yf.download('BTC-USD', start='2020-01-01', end='2025-01-01', auto_adjust=False, progress=False)
    if isinstance(df.columns, pd.MultiIndex):
        df = df.droplevel(1, axis=1)

    print(f"Fetched data: {df.shape}")

    cerebro = bt.Cerebro()
    data = bt.feeds.PandasData(dataname=df)
    cerebro.adddata(data)
    cerebro.broker.setcash(10000.0)
    cerebro.broker.setcommission(commission=0.001)
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe', timeframe=bt.TimeFrame.Days, annualize=True, riskfreerate=0.0)
    cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')

    print("Testing parameter combinations...")
    cerebro.optstrategy(
        MomentumIgnitionStrategy,
        consolidation_period=[20, 30, 40],
        consolidation_threshold=[0.03, 0.05, 0.07],
        roc_period=[5, 7, 10],
        roc_breakout_std=[1.0, 1.5, 2.0],
        atr_stop_multiplier=[2.0, 3.0, 4.0]
    )

    stratruns = cerebro.run(maxcpus=1)  # Force single-threaded to avoid multiprocessing issues
    print(f"Optimization complete! Tested {len(stratruns)} combinations.")

    # Find best parameters
    best_result = None
    best_sharpe = -999

    for run in stratruns:
        strategy = run[0]
        sharpe_analysis = strategy.analyzers.sharpe.get_analysis()
        returns_analysis = strategy.analyzers.returns.get_analysis()
        
        sharpe_ratio = sharpe_analysis.get('sharperatio', None)
        if sharpe_ratio is not None and not np.isnan(sharpe_ratio) and sharpe_ratio > best_sharpe:
            best_sharpe = sharpe_ratio
            best_result = {
                'consolidation_period': strategy.p.consolidation_period,
                'consolidation_threshold': strategy.p.consolidation_threshold,
                'roc_period': strategy.p.roc_period,
                'roc_breakout_std': strategy.p.roc_breakout_std,
                'atr_stop_multiplier': strategy.p.atr_stop_multiplier,
                'sharpe': sharpe_ratio,
                'return': returns_analysis.get('rtot', 0) * 100
            }

    if best_result:
        print(f"\nBEST PARAMETERS FOUND:")
        print(f"Consolidation Period: {best_result['consolidation_period']}")
        print(f"Consolidation Threshold: {best_result['consolidation_threshold']}")
        print(f"ROC Period: {best_result['roc_period']}")
        print(f"ROC Breakout Std: {best_result['roc_breakout_std']}")
        print(f"ATR Stop Multiplier: {best_result['atr_stop_multiplier']}")
        print(f"Sharpe Ratio: {best_result['sharpe']:.3f}")
        print(f"Total Return: {best_result['return']:.1f}%")
    else:
        print("No valid results found, using defaults")
        best_result = {
            'consolidation_period': 30,
            'consolidation_threshold': 0.05,
            'roc_period': 7,
            'roc_breakout_std': 1.0,
            'atr_stop_multiplier': 3.0
        }

    # ROLLING BACKTEST
    print(f"\n{'='*60}")
    print("RUNNING YEARLY ROLLING BACKTESTS")
    print(f"{'='*60}")

    strategy_params = {
        'consolidation_period': best_result['consolidation_period'],
        'consolidation_threshold': best_result['consolidation_threshold'], 
        'roc_period': best_result['roc_period'],
        'roc_breakout_std': best_result['roc_breakout_std'],
        'atr_stop_multiplier': best_result['atr_stop_multiplier'],
        'order_percentage': 0.95,
        'printlog': False
    }

    results = []
    start_dt = pd.to_datetime("2018-01-01")
    end_dt = pd.to_datetime("2025-01-01")
    current_start = start_dt

    while True:
        current_end = current_start + rd.relativedelta(months=12)
        if current_end > end_dt:
            break
            
        print(f"Period: {current_start.date()} to {current_end.date()}")
        
        data = yf.download("BTC-USD", start=current_start, end=current_end, auto_adjust=False, progress=False)
        if data.empty or len(data) < 90:
            current_start += rd.relativedelta(months=12)
            continue
            
        if isinstance(data.columns, pd.MultiIndex):
            data = data.droplevel(1, 1)
        
        start_price = data['Close'].iloc[0]
        end_price = data['Close'].iloc[-1]
        benchmark_return = (end_price - start_price) / start_price
        
        feed = bt.feeds.PandasData(dataname=data)
        cerebro = bt.Cerebro()
        cerebro.addstrategy(MomentumIgnitionStrategy, **strategy_params)
        cerebro.adddata(feed)
        cerebro.broker.setcash(100000)
        cerebro.broker.setcommission(commission=0.001)
        
        start_val = cerebro.broker.getvalue()
        result = cerebro.run()
        final_val = cerebro.broker.getvalue()
        strategy_return = (final_val - start_val) / start_val
        
        strategy_instance = result[0]
        trades = getattr(strategy_instance, 'trade_count', 0)
        long_entries = getattr(strategy_instance, 'long_entries', 0)
        short_entries = getattr(strategy_instance, 'short_entries', 0)
        profitable = getattr(strategy_instance, 'profitable_trades', 0)
        
        results.append({
            'year': current_start.year,
            'strategy_return': strategy_return,
            'benchmark_return': benchmark_return,
            'trades': trades,
            'long_entries': long_entries,
            'short_entries': short_entries,
            'profitable_trades': profitable
        })
        
        print(f"Strategy: {strategy_return:.1%} | Buy&Hold: {benchmark_return:.1%} | Trades: {trades}")
        current_start += rd.relativedelta(months=12)

    # RESULTS & PLOTTING
    results_df = pd.DataFrame(results)

    if not results_df.empty:
        print(f"\n{'='*80}")
        print("YEARLY RESULTS")
        print(f"{'='*80}")
        print("Year | Strategy | Buy&Hold | Outperf | Trades | Long | Short | WinRate")
        print("-" * 80)
        
        for _, row in results_df.iterrows():
            strat_ret = row['strategy_return'] * 100
            bench_ret = row['benchmark_return'] * 100
            outperf = strat_ret - bench_ret
            win_rate = (row['profitable_trades'] / max(1, row['trades'])) * 100
            
            print(f"{int(row['year'])} | {strat_ret:7.1f}% | {bench_ret:7.1f}% | {outperf:+6.1f}% | "
                  f"{int(row['trades']):6d} | {int(row['long_entries']):4d} | {int(row['short_entries']):5d} | {win_rate:6.1f}%")
        
        # PLOTS
        fig, axes = plt.subplots(2, 2, figsize=(16, 10))
        fig.suptitle('Momentum Ignition Strategy Analysis', fontsize=16, fontweight='bold')
        
        # Cumulative performance
        strategy_cumulative = (1 + results_df['strategy_return']).cumprod()
        benchmark_cumulative = (1 + results_df['benchmark_return']).cumprod()
        
        ax1 = axes[0, 0]
        ax1.plot(results_df['year'], strategy_cumulative, 'o-', linewidth=3, color='purple', label='Strategy')
        ax1.plot(results_df['year'], benchmark_cumulative, 's-', linewidth=3, color='orange', label='Buy & Hold')
        ax1.set_title('Cumulative Performance')
        ax1.set_ylabel('Cumulative Return')
        ax1.legend()
        ax1.grid(True, alpha=0.3)
        
        # Annual returns
        ax2 = axes[0, 1]
        x_pos = range(len(results_df))
        width = 0.35
        ax2.bar([x - width/2 for x in x_pos], results_df['strategy_return'] * 100, width, 
                label='Strategy', color='purple', alpha=0.8)
        ax2.bar([x + width/2 for x in x_pos], results_df['benchmark_return'] * 100, width,
                label='Buy & Hold', color='orange', alpha=0.8)
        ax2.set_title('Annual Returns')
        ax2.set_ylabel('Return (%)')
        ax2.set_xticks(x_pos)
        ax2.set_xticklabels([int(year) for year in results_df['year']])
        ax2.legend()
        ax2.grid(True, alpha=0.3)
        
        # Long vs Short
        ax3 = axes[1, 0]
        ax3.bar(x_pos, results_df['long_entries'], label='Long', color='green', alpha=0.8)
        ax3.bar(x_pos, results_df['short_entries'], bottom=results_df['long_entries'], 
                label='Short', color='red', alpha=0.8)
        ax3.set_title('Long vs Short Entries')
        ax3.set_ylabel('Number of Entries')
        ax3.set_xticks(x_pos)
        ax3.set_xticklabels([int(year) for year in results_df['year']])
        ax3.legend()
        ax3.grid(True, alpha=0.3)
        
        # Summary stats
        ax4 = axes[1, 1]
        ax4.axis('off')
        
        total_strategy_return = strategy_cumulative.iloc[-1] - 1
        total_benchmark_return = benchmark_cumulative.iloc[-1] - 1
        total_trades = results_df['trades'].sum()
        total_profitable = results_df['profitable_trades'].sum()
        
        summary_text = f"""
SUMMARY STATISTICS

Total Strategy Return: {total_strategy_return:.1%}
Total Buy & Hold Return: {total_benchmark_return:.1%}
Outperformance: {total_strategy_return - total_benchmark_return:+.1%}

Total Trades: {int(total_trades)}
Overall Win Rate: {total_profitable/max(1,total_trades):.1%}
Long Entries: {int(results_df['long_entries'].sum())}
Short Entries: {int(results_df['short_entries'].sum())}

Strategy wins in {(results_df['strategy_return'] > results_df['benchmark_return']).sum()}/{len(results_df)} years
        """
        
        ax4.text(0.1, 0.9, summary_text, transform=ax4.transAxes, va='top', ha='left',
                  fontsize=12, fontfamily='monospace',
                  bbox=dict(boxstyle="round,pad=0.5", facecolor="lightgray", alpha=0.8))
        
        plt.tight_layout()
        plt.show()
        
        # Final verdict
        print(f"\n{'='*60}")
        if total_strategy_return > total_benchmark_return:
            print(f"STRATEGY WINS by {total_strategy_return - total_benchmark_return:+.1%}!")
        else:
            print(f"Strategy underperformed by {total_strategy_return - total_benchmark_return:.1%}")
        print(f"{'='*60}")

    else:
        print("No results generated!")

    print("ANALYSIS COMPLETE!")

if __name__ == '__main__':
    multiprocessing.freeze_support() # Required for Windows
    run_momentum_analysis()

Parameters (params):

Initialization (__init__):

Order and Trade Notifications (notify_order, notify_trade):

Main Logic (next):

The next method contains the core trading logic, executed on each new bar of data:

  1. Pending Order Check: if self.order: return ensures no new orders are placed while one is pending.
  2. Entry Logic (if not self.position):
    • Consolidation Check: is_consolidating is true if the normalized price standard deviation is below consolidation_threshold.
    • Macro Trend Check: is_macro_uptrend (close > trend_sma) or is_macro_downtrend (close < trend_sma).
    • Momentum Breakout Check: If consolidating, it defines roc_upper_band and roc_lower_band based on roc_ma and roc_stddev. is_mom_breakout_up is true if roc[0] crosses above roc_upper_band; is_mom_breakout_down is true if roc[0] crosses below roc_lower_band.
    • Order Placement: If is_macro_uptrend AND is_mom_breakout_up, a buy order is placed. If is_macro_downtrend AND is_mom_breakout_down, a sell order is placed. Position size is determined by order_percentage.
  3. Exit Logic (elif self.position - ATR Trailing Stop):
    • Long Position: self.highest_price_since_entry is updated. A new_stop is calculated using atr_stop_multiplier. self.stop_price is updated to always move in the profitable direction (max for long). If data.close[0] falls below self.stop_price, the position is closed, and self.atr_stop_exits is incremented.
    • Short Position: self.lowest_price_since_entry is updated. A new_stop is calculated. self.stop_price is updated to always move in the profitable direction (min for short). If data.close[0] rises above self.stop_price, the position is closed, and self.atr_stop_exits is incremented.

Comprehensive Analysis Framework

This section presents the full code for running optimization, a rolling backtest, and generating detailed results with plots.

def run_momentum_analysis():
    # OPTIMIZATION
    print("="*60)
    print("MOMENTUM IGNITION STRATEGY OPTIMIZATION")
    print("="*60)

    df = yf.download('BTC-USD', start='2020-01-01', end='2025-01-01', auto_adjust=False, progress=False)
    if isinstance(df.columns, pd.MultiIndex):
        df = df.droplevel(1, axis=1)

    print(f"Fetched data: {df.shape}")

    cerebro = bt.Cerebro()
    data = bt.feeds.PandasData(dataname=df)
    cerebro.adddata(data)
    cerebro.broker.setcash(10000.0)
    cerebro.broker.setcommission(commission=0.001)
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe', timeframe=bt.TimeFrame.Days, annualize=True, riskfreerate=0.0)
    cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')

    print("Testing parameter combinations...")
    cerebro.optstrategy(
        MomentumIgnitionStrategy,
        consolidation_period=[20, 30, 40],
        consolidation_threshold=[0.03, 0.05, 0.07],
        roc_period=[5, 7, 10],
        roc_breakout_std=[1.0, 1.5, 2.0],
        atr_stop_multiplier=[2.0, 3.0, 4.0]
    )

    stratruns = cerebro.run(maxcpus=1)  # Force single-threaded to avoid multiprocessing issues
    print(f"Optimization complete! Tested {len(stratruns)} combinations.")

    # Find best parameters
    best_result = None
    best_sharpe = -999

    for run in stratruns:
        strategy = run[0]
        sharpe_analysis = strategy.analyzers.sharpe.get_analysis()
        returns_analysis = strategy.analyzers.returns.get_analysis()
        
        sharpe_ratio = sharpe_analysis.get('sharperatio', None)
        if sharpe_ratio is not None and not np.isnan(sharpe_ratio) and sharpe_ratio > best_sharpe:
            best_sharpe = sharpe_ratio
            best_result = {
                'consolidation_period': strategy.p.consolidation_period,
                'consolidation_threshold': strategy.p.consolidation_threshold,
                'roc_period': strategy.p.roc_period,
                'roc_breakout_std': strategy.p.roc_breakout_std,
                'atr_stop_multiplier': strategy.p.atr_stop_multiplier,
                'sharpe': sharpe_ratio,
                'return': returns_analysis.get('rtot', 0) * 100
            }

    if best_result:
        print(f"\nBEST PARAMETERS FOUND:")
        print(f"Consolidation Period: {best_result['consolidation_period']}")
        print(f"Consolidation Threshold: {best_result['consolidation_threshold']}")
        print(f"ROC Period: {best_result['roc_period']}")
        print(f"ROC Breakout Std: {best_result['roc_breakout_std']}")
        print(f"ATR Stop Multiplier: {best_result['atr_stop_multiplier']}")
        print(f"Sharpe Ratio: {best_result['sharpe']:.3f}")
        print(f"Total Return: {best_result['return']:.1f}%")
    else:
        print("No valid results found, using defaults")
        best_result = {
            'consolidation_period': 30,
            'consolidation_threshold': 0.05,
            'roc_period': 7,
            'roc_breakout_std': 1.0,
            'atr_stop_multiplier': 3.0
        }

    # ROLLING BACKTEST
    print(f"\n{'='*60}")
    print("RUNNING YEARLY ROLLING BACKTESTS")
    print(f"{'='*60}")

    strategy_params = {
        'consolidation_period': best_result['consolidation_period'],
        'consolidation_threshold': best_result['consolidation_threshold'], 
        'roc_period': best_result['roc_period'],
        'roc_breakout_std': best_result['roc_breakout_std'],
        'atr_stop_multiplier': best_result['atr_stop_multiplier'],
        'order_percentage': 0.95,
        'printlog': False
    }

    results = []
    start_dt = pd.to_datetime("2018-01-01")
    end_dt = pd.to_datetime("2025-01-01")
    current_start = start_dt

    while True:
        current_end = current_start + rd.relativedelta(months=12)
        if current_end > end_dt:
            break
            
        print(f"Period: {current_start.date()} to {current_end.date()}")
        
        data = yf.download("BTC-USD", start=current_start, end=current_end, auto_adjust=False, progress=False)
        if data.empty or len(data) < 90:
            current_start += rd.relativedelta(months=12)
            continue
            
        if isinstance(data.columns, pd.MultiIndex):
            data = data.droplevel(1, 1)
        
        start_price = data['Close'].iloc[0]
        end_price = data['Close'].iloc[-1]
        benchmark_return = (end_price - start_price) / start_price
        
        feed = bt.feeds.PandasData(dataname=data)
        cerebro = bt.Cerebro()
        cerebro.addstrategy(MomentumIgnitionStrategy, **strategy_params)
        cerebro.adddata(feed)
        cerebro.broker.setcash(100000)
        cerebro.broker.setcommission(commission=0.001)
        
        start_val = cerebro.broker.getvalue()
        result = cerebro.run()
        final_val = cerebro.broker.getvalue()
        strategy_return = (final_val - start_val) / start_val
        
        strategy_instance = result[0]
        trades = getattr(strategy_instance, 'trade_count', 0)
        long_entries = getattr(strategy_instance, 'long_entries', 0)
        short_entries = getattr(strategy_instance, 'short_entries', 0)
        profitable = getattr(strategy_instance, 'profitable_trades', 0)
        
        results.append({
            'year': current_start.year,
            'strategy_return': strategy_return,
            'benchmark_return': benchmark_return,
            'trades': trades,
            'long_entries': long_entries,
            'short_entries': short_entries,
            'profitable_trades': profitable
        })
        
        print(f"Strategy: {strategy_return:.1%} | Buy&Hold: {benchmark_return:.1%} | Trades: {trades}")
        current_start += rd.relativedelta(months=12)

    # RESULTS & PLOTTING
    results_df = pd.DataFrame(results)

    if not results_df.empty:
        print(f"\n{'='*80}")
        print("YEARLY RESULTS")
        print(f"{'='*80}")
        print("Year | Strategy | Buy&Hold | Outperf | Trades | Long | Short | WinRate")
        print("-" * 80)
        
        for _, row in results_df.iterrows():
            strat_ret = row['strategy_return'] * 100
            bench_ret = row['benchmark_return'] * 100
            outperf = strat_ret - bench_ret
            win_rate = (row['profitable_trades'] / max(1, row['trades'])) * 100
            
            print(f"{int(row['year'])} | {strat_ret:7.1f}% | {bench_ret:7.1f}% | {outperf:+6.1f}% | "
                  f"{int(row['trades']):6d} | {int(row['long_entries']):4d} | {int(row['short_entries']):5d} | {win_rate:6.1f}%")
        
        # PLOTS
        fig, axes = plt.subplots(2, 2, figsize=(16, 10))
        fig.suptitle('Momentum Ignition Strategy Analysis', fontsize=16, fontweight='bold')
        
        # Cumulative performance
        strategy_cumulative = (1 + results_df['strategy_return']).cumprod()
        benchmark_cumulative = (1 + results_df['benchmark_return']).cumprod()
        
        ax1 = axes[0, 0]
        ax1.plot(results_df['year'], strategy_cumulative, 'o-', linewidth=3, color='purple', label='Strategy')
        ax1.plot(results_df['year'], benchmark_cumulative, 's-', linewidth=3, color='orange', label='Buy & Hold')
        ax1.set_title('Cumulative Performance')
        ax1.set_ylabel('Cumulative Return')
        ax1.legend()
        ax1.grid(True, alpha=0.3)
        
        # Annual returns
        ax2 = axes[0, 1]
        x_pos = range(len(results_df))
        width = 0.35
        ax2.bar([x - width/2 for x in x_pos], results_df['strategy_return'] * 100, width, 
                label='Strategy', color='purple', alpha=0.8)
        ax2.bar([x + width/2 for x in x_pos], results_df['benchmark_return'] * 100, width,
                label='Buy & Hold', color='orange', alpha=0.8)
        ax2.set_title('Annual Returns')
        ax2.set_ylabel('Return (%)')
        ax2.set_xticks(x_pos)
        ax2.set_xticklabels([int(year) for year in results_df['year']])
        ax2.legend()
        ax2.grid(True, alpha=0.3)
        
        # Long vs Short
        ax3 = axes[1, 0]
        ax3.bar(x_pos, results_df['long_entries'], label='Long', color='green', alpha=0.8)
        ax3.bar(x_pos, results_df['short_entries'], bottom=results_df['long_entries'], 
                label='Short', color='red', alpha=0.8)
        ax3.set_title('Long vs Short Entries')
        ax3.set_ylabel('Number of Entries')
        ax3.set_xticks(x_pos)
        ax3.set_xticklabels([int(year) for year in results_df['year']])
        ax3.legend()
        ax3.grid(True, alpha=0.3)
        
        # Summary stats
        ax4 = axes[1, 1]
        ax4.axis('off')
        
        total_strategy_return = strategy_cumulative.iloc[-1] - 1
        total_benchmark_return = benchmark_cumulative.iloc[-1] - 1
        total_trades = results_df['trades'].sum()
        total_profitable = results_df['profitable_trades'].sum()
        
        summary_text = f"""
SUMMARY STATISTICS

Total Strategy Return: {total_strategy_return:.1%}
Total Buy & Hold Return: {total_benchmark_return:.1%}
Outperformance: {total_strategy_return - total_benchmark_return:+.1%}

Total Trades: {int(total_trades)}
Overall Win Rate: {total_profitable/max(1,total_trades):.1%}
Long Entries: {int(results_df['long_entries'].sum())}
Short Entries: {int(results_df['short_entries'].sum())}

Strategy wins in {(results_df['strategy_return'] > results_df['benchmark_return']).sum()}/{len(results_df)} years
        """
        
        ax4.text(0.1, 0.9, summary_text, transform=ax4.transAxes, va='top', ha='left',
                  fontsize=12, fontfamily='monospace',
                  bbox=dict(boxstyle="round,pad=0.5", facecolor="lightgray", alpha=0.8))
        
        plt.tight_layout()
        plt.show()
        
        # Final verdict
        print(f"\n{'='*60}")
        if total_strategy_return > total_benchmark_return:
            print(f"STRATEGY WINS by {total_strategy_return - total_benchmark_return:+.1%}!")
        else:
            print(f"Strategy underperformed by {total_strategy_return - total_benchmark_return:.1%}")
        print(f"{'='*60}")

    else:
        print("No results generated!")

    print("ANALYSIS COMPLETE!")

Optimization (run_momentum_analysis - Optimization Section):

This part of run_momentum_analysis is responsible for finding the best combination of parameters for the MomentumIgnitionStrategy.

Rolling Backtest (run_momentum_analysis - Rolling Backtest Section):

After optimization, this section runs a rolling backtest with the identified best parameters to assess the strategy’s performance consistency over a broader and more representative historical period.

Results and Plotting

Pasted image 20250727172653.png

This comprehensive framework allows for thorough testing and evaluation of the Momentum Ignition Strategy, from finding optimal parameters to understanding its performance and characteristics across various market conditions.