← Back to Home
Rolling Backtest of a Multi-Timeframe Pivot Point Strategy with Volume and Momentum Confirmation

Rolling Backtest of a Multi-Timeframe Pivot Point Strategy with Volume and Momentum Confirmation

This article presents a dynamic trading strategy, PivotPointStrategy, built upon the principles of classical pivot points, enhanced with volume and momentum confirmations. The strategy intelligently adapts to different timeframes by calculating daily, weekly, and optionally monthly pivot levels, and trades based on price interaction with these crucial support and resistance zones. A fixed stop-loss mechanism is also integrated for risk management.

1. The Pivot Point Strategy Concept

The PivotPointStrategy leverages the predictive power of pivot points, which are widely used by traders to identify potential turning points and key price levels. The strategy distinguishes itself by:

Core Trading Logic:

The strategy aims for two primary types of trades around pivot levels:

  1. Bounce Trades: When the price approaches or touches a support (S1, S2, S3) or resistance (R1, R2, R3) level, it anticipates a reversal.
    • At Support: If price interacts with a support level, it looks for a long entry (or closes a short position), expecting the price to bounce upwards.
    • At Resistance: If price interacts with a resistance level, it looks for a short entry (or closes a long position), expecting the price to bounce downwards.
  2. Breakout Trades: When the price definitively breaks through a support or resistance level, it anticipates a continuation of the new trend.
    • Resistance Breakout: If price breaks above a resistance level, it looks for a long entry (or closes a short position), expecting the price to continue its upward move.
    • Support Breakout: If price breaks below a support level, it looks for a short entry (or closes a long position), expecting the price to continue its downward move.

All entry and exit signals are subject to the volume and momentum confirmation filters.

2. Calculating Pivot Points

The strategy relies on dynamically calculating pivot points (PP) and their associated support (S1, S2, S3) and resistance (R1, R2, R3) levels based on the previous period’s High, Low, and Close prices.

The classical formulas for pivot points are:

Where \(H\), \(L\), and \(C\) are the High, Low, and Close prices of the previous period, respectively.

3. The PivotPointStrategy Implementation

Here are the key components of the strategy:

import backtrader as bt
import yfinance as yf
import pandas as pd
import numpy as np
from datetime import datetime, timedelta

class PivotPointStrategy(bt.Strategy):
    params = (
        ('use_daily', True),       # Use daily pivot points
        ('use_weekly', True),      # Use weekly pivot points
        ('use_monthly', False),    # Use monthly pivot points
        ('bounce_threshold', 0.01), # 1% threshold for level interaction (near/touch)
        ('breakout_threshold', 0.03), # 3% threshold for breakouts
        ('volume_multiplier', 1.2), # Volume confirmation multiplier (1.2x average)
        ('volume_period', 30),      # Volume average period
        ('rsi_period', 14),         # RSI for momentum confirmation
        ('stop_loss_pct', 0.05),    # 5% stop loss
    )
    
    def __init__(self):
        # Data feeds
        self.high = self.data.high
        self.low = self.data.low
        self.close = self.data.close
        self.open = self.data.open
        self.volume = self.data.volume
        
        # Technical indicators
        self.volume_sma = bt.indicators.SMA(self.volume, period=self.params.volume_period)
        self.rsi = bt.indicators.RSI(period=self.params.rsi_period)
        
        # Storage for calculated pivot levels (keyed by date/week/month start)
        self.daily_pivots = {}
        self.weekly_pivots = {}
        self.monthly_pivots = {}
        
        # OHLC data accumulators for multi-period pivot calculations
        self.daily_ohlc = {'high': 0, 'low': 0, 'close': 0}
        self.weekly_ohlc = {'high': 0, 'low': 0, 'close': 0}
        self.monthly_ohlc = {'high': 0, 'low': 0, 'close': 0}
        
        # Track last calculation dates to determine when to reset OHLC for new periods
        self.last_daily_calc = None
        self.last_weekly_calc = None
        self.last_monthly_calc = None
        
        # Order tracking
        self.order = None      # To prevent multiple orders
        self.stop_order = None # To manage the stop-loss order

    def calculate_pivot_levels(self, high, low, close):
        """Calculates classic pivot point levels."""
        pp = (high + low + close) / 3
        s1 = (2 * pp) - high
        s2 = pp - (high - low)
        s3 = low - 2 * (high - pp)
        r1 = (2 * pp) - low
        r2 = pp + (high - low)
        r3 = high + 2 * (pp - low)
        return {'PP': pp, 'S1': s1, 'S2': s2, 'S3': s3,
                'R1': r1, 'R2': r2, 'R3': r3}

    def update_ohlc_data(self):
        """
        Aggregates OHLC data for daily, weekly, and monthly pivot calculations.
        Calculates new pivots when a period rolls over.
        """
        current_date = self.data.datetime.date(0)
        current_high = self.high[0]
        current_low = self.low[0]
        current_close = self.close[0]
        
        # Daily OHLC and Pivot Calculation
        if self.last_daily_calc != current_date:
            if self.last_daily_calc is not None:
                # If it's a new day, calculate pivots from the *previous* day's aggregated OHLC
                self.daily_pivots[current_date] = self.calculate_pivot_levels(
                    self.daily_ohlc['high'], self.daily_ohlc['low'], self.daily_ohlc['close']
                )
            # Reset OHLC for the new day
            self.daily_ohlc = {'high': current_high, 'low': current_low, 'close': current_close}
            self.last_daily_calc = current_date
        else:
            # Continue accumulating OHLC for the current day
            self.daily_ohlc['high'] = max(self.daily_ohlc['high'], current_high)
            self.daily_ohlc['low'] = min(self.daily_ohlc['low'], current_low)
            self.daily_ohlc['close'] = current_close # Always use current close for daily pivot calc

        # Weekly OHLC and Pivot Calculation (Week starts on Monday, weekday() returns 0 for Monday)
        week_start = current_date - timedelta(days=current_date.weekday())
        if self.last_weekly_calc != week_start:
            if self.last_weekly_calc is not None:
                self.weekly_pivots[week_start] = self.calculate_pivot_levels(
                    self.weekly_ohlc['high'], self.weekly_ohlc['low'], self.weekly_ohlc['close']
                )
            self.weekly_ohlc = {'high': current_high, 'low': current_low, 'close': current_close}
            self.last_weekly_calc = week_start
        else:
            self.weekly_ohlc['high'] = max(self.weekly_ohlc['high'], current_high)
            self.weekly_ohlc['low'] = min(self.weekly_ohlc['low'], current_low)
            self.weekly_ohlc['close'] = current_close

        # Monthly OHLC and Pivot Calculation
        month_start = current_date.replace(day=1)
        if self.last_monthly_calc != month_start:
            if self.last_monthly_calc is not None:
                self.monthly_pivots[month_start] = self.calculate_pivot_levels(
                    self.monthly_ohlc['high'], self.monthly_ohlc['low'], self.monthly_ohlc['close']
                )
            self.monthly_ohlc = {'high': current_high, 'low': current_low, 'close': current_close}
            self.last_monthly_calc = month_start
        else:
            self.monthly_ohlc['high'] = max(self.monthly_ohlc['high'], current_high)
            self.monthly_ohlc['low'] = min(self.monthly_ohlc['low'], current_low)
            self.monthly_ohlc['close'] = current_close

    def get_current_pivot_levels(self):
        """Retrieves and consolidates all active pivot levels for the current bar."""
        current_date = self.data.datetime.date(0)
        levels = []
        
        # Add daily pivots if enabled and available for the current day (yesterday's calculation)
        if self.params.use_daily and (current_date - timedelta(days=1)) in self.daily_pivots:
            daily_levels_date = current_date - timedelta(days=1) # Pivots are for today, based on yesterday's data
            if daily_levels_date in self.daily_pivots:
                 daily = self.daily_pivots[daily_levels_date]
                 for level_name, level_value in daily.items():
                     levels.append((level_value, 'daily', level_name))
        
        # Add weekly pivots if enabled and available
        if self.params.use_weekly:
            prev_week_start = current_date - timedelta(days=current_date.weekday() + 7) # Start of previous week
            if prev_week_start in self.weekly_pivots:
                weekly = self.weekly_pivots[prev_week_start]
                for level_name, level_value in weekly.items():
                    levels.append((level_value, 'weekly', level_name))
        
        # Add monthly pivots if enabled and available
        if self.params.use_monthly:
            prev_month_start = (current_date.replace(day=1) - timedelta(days=1)).replace(day=1) # Start of previous month
            if prev_month_start in self.monthly_pivots:
                monthly = self.monthly_pivots[prev_month_start]
                for level_name, level_value in monthly.items():
                    levels.append((level_value, 'monthly', level_name))
        
        # Sort levels for easier searching (e.g., finding nearest)
        return sorted(levels, key=lambda x: x[0])

    def check_level_interaction(self, price, high, low):
        """
        Determines if the current bar's price action interacts with any pivot levels
        (bounce or breakout).
        """
        levels = self.get_current_pivot_levels()
        
        for level_price, timeframe, level_name in levels:
            # Check for 'touch' or 'near' interaction (potential bounce)
            distance_pct = abs(price - level_price) / level_price
            
            if distance_pct <= self.params.bounce_threshold:
                # If current bar touches or crosses the level
                if (low <= level_price <= high):
                    return 'touch', level_price, timeframe, level_name
                # Or if very close
                elif distance_pct <= self.params.bounce_threshold / 2:
                    return 'near', level_price, timeframe, level_name
            
            # Check for breakouts
            elif distance_pct <= self.params.breakout_threshold:
                # Support breakout: Close below level, and level was within bar range
                if level_name.startswith('S') and price < level_price and low <= level_price <= high:
                    return 'support_break', level_price, timeframe, level_name
                # Resistance breakout: Close above level, and level was within bar range
                elif level_name.startswith('R') and price > level_price and low <= level_price <= high:
                    return 'resistance_break', level_price, timeframe, level_name
        
        return None, None, None, None # No significant interaction

    def volume_confirmation(self):
        """Checks if current volume is above the average volume by a multiplier."""
        # Ensure enough data for volume SMA and handle division by zero
        if np.isnan(self.volume_sma[0]) or self.volume_sma[0] == 0:
            return True # No average, assume confirmation (or add a warning)
        return self.volume[0] > self.volume_sma[0] * self.params.volume_multiplier

    def momentum_confirmation(self, trade_direction):
        """Checks RSI for momentum confirmation."""
        # Ensure RSI has enough data
        if np.isnan(self.rsi[0]):
            return True # No RSI, assume confirmation (or add a warning)
            
        if trade_direction == 'long':
            # For long, RSI should not be overbought (above 70) and not extremely oversold (below 30)
            return self.rsi[0] > 30 and self.rsi[0] < 70 
        elif trade_direction == 'short':
            # For short, RSI should not be oversold (below 30) and not extremely overbought (above 70)
            return self.rsi[0] > 30 and self.rsi[0] < 70 
        return True # Default if direction is not specified

    def notify_order(self, order):
        # Handles order completion (execution) or failure (canceled/rejected)
        if order.status in [order.Completed]:
            if order.isbuy() and self.position.size > 0: # Check if it's an entry buy
                stop_price = order.executed.price * (1 - self.params.stop_loss_pct)
                self.stop_order = self.sell(exectype=bt.Order.Stop, price=stop_price, size=self.position.size)
                self.log(f'BUY EXECUTED, Price: {order.executed.price:.2f}, Setting Stop Loss at {stop_price:.2f}')
            elif order.issell() and self.position.size < 0: # Check if it's an entry sell (short)
                stop_price = order.executed.price * (1 + self.params.stop_loss_pct)
                self.stop_order = self.buy(exectype=bt.Order.Stop, price=stop_price, size=abs(self.position.size))
                self.log(f'SELL EXECUTED, Price: {order.executed.price:.2f}, Setting Stop Loss at {stop_price:.2f}')
            elif order.isbuy() and self.position.size == 0 and self.stop_order and order.ref == self.stop_order.ref: # Stop-loss buy (cover short)
                self.log(f'STOP LOSS (COVER SHORT) EXECUTED, Price: {order.executed.price:.2f}')
            elif order.issell() and self.position.size == 0 and self.stop_order and order.ref == self.stop_order.ref: # Stop-loss sell (exit long)
                self.log(f'STOP LOSS (EXIT LONG) EXECUTED, Price: {order.executed.price:.2f}')

            self.order = None # Clear general order reference
            if self.stop_order and order.ref == self.stop_order.ref: # If the completed order was the stop order itself
                self.stop_order = None # Clear stop order reference
        
        elif order.status in [order.Canceled, order.Rejected, order.Margin]:
            self.log(f'Order Canceled/Rejected/Margin: Status {order.getstatusname()}')
            self.order = None # Clear entry order reference
            if self.stop_order and order.ref == self.stop_order.ref:
                self.log("WARNING: Stop-Loss Order failed!", doprint=True)
                self.stop_order = None


    def next(self):
        # Ensure no pending orders and enough data for indicators
        if self.order is not None:
            return
        
        # Minimum data needed for indicators to warm up
        min_data_needed = max(self.params.volume_period, self.params.rsi_period)
        if len(self.data) < min_data_needed + 1: # +1 for current bar data
            return

        # Update OHLC data and calculate pivots for the *next* day/week/month
        # (These are pivot points for the *current* bar's date, based on previous period's OHLC)
        self.update_ohlc_data() 
        
        current_price = self.close[0]
        current_high = self.high[0]
        current_low = self.low[0]
        
        # Check interaction with pivot levels
        interaction, level_price, timeframe, level_name = self.check_level_interaction(
            current_price, current_high, current_low
        )
        
        if interaction is None: # No significant interaction with any level
            return
        
        # Trading logic based on pivot level interactions
        
        # BOUNCE STRATEGY (Price touches/nears level and reverses)
        if interaction == 'touch' or interaction == 'near':
            if level_name.startswith('S'): # Support level - expect bounce UP (buy signal)
                if self.momentum_confirmation('long') and self.volume_confirmation():
                    if self.position.size < 0: # Close existing short position
                        if self.stop_order is not None: self.cancel(self.stop_order)
                        self.order = self.close()
                        self.log(f'Close SHORT on {level_name} ({timeframe}) bounce signal at {current_price:.2f}')
                    elif not self.position: # Open new long position
                        self.order = self.buy()
                        self.log(f'BUY on {level_name} ({timeframe}) bounce signal at {current_price:.2f}')
            
            elif level_name.startswith('R'): # Resistance level - expect bounce DOWN (sell/short signal)
                if self.momentum_confirmation('short') and self.volume_confirmation():
                    if self.position.size > 0: # Close existing long position
                        if self.stop_order is not None: self.cancel(self.stop_order)
                        self.order = self.close()
                        self.log(f'Close LONG on {level_name} ({timeframe}) bounce signal at {current_price:.2f}')
                    elif not self.position: # Open new short position
                        self.order = self.sell()
                        self.log(f'SELL on {level_name} ({timeframe}) bounce signal at {current_price:.2f}')
        
        # BREAKOUT STRATEGY (Price breaks through level with confirmation)
        elif interaction == 'resistance_break': # Break above resistance - go long
            if self.momentum_confirmation('long') and self.volume_confirmation():
                if self.position.size < 0: # Close existing short position
                    if self.stop_order is not None: self.cancel(self.stop_order)
                    self.order = self.close()
                    self.log(f'Close SHORT on {level_name} ({timeframe}) breakout signal at {current_price:.2f}')
                elif not self.position: # Open new long position
                    self.order = self.buy()
                    self.log(f'BUY on {level_name} ({timeframe}) breakout signal at {current_price:.2f}')
        
        elif interaction == 'support_break': # Break below support - go short
            if self.momentum_confirmation('short') and self.volume_confirmation():
                if self.position.size > 0: # Close existing long position
                    if self.stop_order is not None: self.cancel(self.stop_order)
                    self.order = self.close()
                    self.log(f'Close LONG on {level_name} ({timeframe}) breakout signal at {current_price:.2f}')
                elif not self.position: # Open new short position
                    self.order = self.sell()
                    self.log(f'SELL on {level_name} ({timeframe}) breakout signal at {current_price:.2f}')

    def stop(self):
        # Optional: Log final portfolio value at the end of the backtest
        print(f'Final Portfolio Value: {self.broker.getvalue():,.2f}')

Explanation of PivotPointStrategy:

4. Backtesting and Analysis

The provided script includes two primary ways to test this strategy:

  1. Single Backtest (for initial testing and plotting):

    if __name__=='__main__':
        # Download data and run backtest (Example for ETH-USD)
        data = yf.download('ETH-USD', '2020-01-01', '2024-01-01', auto_adjust=False) # Use auto_adjust=False as per preference
        data.columns = data.columns.droplevel(1) if isinstance(data.columns, pd.MultiIndex) else data.columns # Droplevel if MultiIndex
        data_feed = bt.feeds.PandasData(dataname=data)
    
        cerebro = bt.Cerebro()
        cerebro.addstrategy(PivotPointStrategy)
        cerebro.adddata(data_feed)
        cerebro.addsizer(bt.sizers.PercentSizer, percents=95)
        cerebro.broker.setcash(100000)
        cerebro.broker.setcommission(commission=0.001)
    
        print(f'Start: ${cerebro.broker.getvalue():,.2f}')
        results = cerebro.run()
        print(f'End: ${cerebro.broker.getvalue():,.2f}')
        print(f'Return: {((cerebro.broker.getvalue() / 100000) - 1) * 100:.2f}%')
    
        # Fix matplotlib plotting issues (already in your code)
        plt.rcParams['figure.max_open_warning'] = 0
        plt.rcParams['agg.path.chunksize'] = 10000
    
        try:
            cerebro.plot(iplot=False, style='candlestick', volume=True) # Added candlestick and volume
            plt.show()
        except Exception as e:
            print(f"Plotting error: {e}")
            print("Strategy completed successfully - plotting skipped")

    This block allows you to quickly run the strategy over a single period and visualize its trades using cerebro.plot().

  2. Rolling Backtest (for robustness evaluation): This method is crucial for understanding a strategy’s consistency across various market conditions, preventing curve-fitting to a single historical period.

    import dateutil.relativedelta as rd # Added import
    import seaborn as sns # Added import
    
    # Define the strategy for the rolling backtest
    strategy = PivotPointStrategy
    
    def run_rolling_backtest(
        ticker="BTC-USD",
        start="2018-01-01",
        end="2025-06-21", # Updated end date to current date
        window_months=3,
        strategy_params=None
    ):
        strategy_params = strategy_params or {}
        all_results = []
        start_dt = pd.to_datetime(start)
        end_dt = pd.to_datetime(end)
        current_start = start_dt
    
        while True:
            current_end = current_start + rd.relativedelta(months=window_months)
            if current_end > end_dt:
                current_end = end_dt 
                if current_start >= current_end:
                    break
    
            print(f"\nROLLING BACKTEST: {current_start.date()} to {current_end.date()}")
    
            # Data download using yfinance, respecting user's preference
            # Using the saved preference: yfinance download with auto_adjust=False and droplevel(axis=1, level=1)
            data = yf.download(ticker, start=current_start, end=current_end, auto_adjust=False, progress=False)
    
            # Apply droplevel if data is a MultiIndex, as per user's preference
            if isinstance(data.columns, pd.MultiIndex):
                data = data.droplevel(1, axis=1)
    
            # Check for sufficient data for strategy warm-up
            # PivotPointStrategy needs data for volume_period and rsi_period.
            # Also, pivot calculations need at least 1 day/week/month of prior data.
            # So, min_bars_needed should consider the longest period for indicators + some buffer for pivot calc.
            vol_period = strategy_params.get('volume_period', PivotPointStrategy.params.volume_period)
            rsi_period = strategy_params.get('rsi_period', PivotPointStrategy.params.rsi_period)
    
            min_bars_needed = max(vol_period, rsi_period) + 30 # Rough buffer for pivots and general stability
    
            if data.empty or len(data) < min_bars_needed:
                print(f"Not enough data for period {current_start.date()} to {current_end.date()} (requires at least {min_bars_needed} bars). Skipping.")
                if current_end == end_dt:
                    break
                current_start = current_end
                continue
    
            feed = bt.feeds.PandasData(dataname=data)
            cerebro = bt.Cerebro()
            cerebro.addstrategy(strategy, **strategy_params)
            cerebro.adddata(feed)
            cerebro.broker.setcash(100000)
            cerebro.broker.setcommission(commission=0.001)
            cerebro.addsizer(bt.sizers.PercentSizer, percents=95)
    
            start_val = cerebro.broker.getvalue()
            cerebro.run()
            final_val = cerebro.broker.getvalue()
            ret = (final_val - start_val) / start_val * 100
    
            all_results.append({
                'start': current_start.date(),
                'end': current_end.date(),
                'return_pct': ret,
                'final_value': final_val,
            })
    
            print(f"Return: {ret:.2f}% | Final Value: {final_val:.2f}")
    
            if current_end == end_dt:
                break
            current_start = current_end
    
        return pd.DataFrame(all_results)
    
    
    def report_stats(df):
        returns = df['return_pct']
        stats = {
            'Mean Return %': np.mean(returns),
            'Median Return %': np.median(returns),
            'Std Dev %': np.std(returns),
            'Min Return %': np.min(returns),
            'Max Return %': np.max(returns),
            'Sharpe Ratio': np.mean(returns) / np.std(returns) if np.std(returns) > 0 else np.nan
        }
        print("\n=== ROLLING BACKTEST STATISTICS ===")
        for k, v in stats.items():
            print(f"{k}: {v:.2f}")
        return stats
    
    def plot_four_charts(df, rolling_sharpe_window=4):
        """
        Generates four analytical plots for rolling backtest results.
        """
        fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(12, 8)) # Adjusted figsize for clarity
    
        periods = list(range(len(df)))
        returns = df['return_pct']
    
        # 1. Period Returns (Top Left)
        colors = ['green' if r >= 0 else 'red' for r in returns]
        ax1.bar(periods, returns, color=colors, alpha=0.7)
        ax1.set_title('Period Returns', fontsize=14, fontweight='bold')
        ax1.set_xlabel('Period')
        ax1.set_ylabel('Return %')
        ax1.axhline(y=0, color='black', linestyle='-', alpha=0.3)
        ax1.grid(True, alpha=0.3)
    
        # 2. Cumulative Returns (Top Right)
        cumulative_returns = (1 + returns / 100).cumprod() * 100 - 100
        ax2.plot(periods, cumulative_returns, marker='o', linewidth=2, markersize=4, color='blue')
        ax2.set_title('Cumulative Returns', fontsize=14, fontweight='bold')
        ax2.set_xlabel('Period')
        ax2.set_ylabel('Cumulative Return %')
        ax2.grid(True, alpha=0.3)
    
        # 3. Rolling Sharpe Ratio (Bottom Left)
        rolling_sharpe = returns.rolling(window=rolling_sharpe_window).apply(
            lambda x: x.mean() / x.std() if x.std() > 0 else np.nan, raw=False
        )
        valid_mask = ~rolling_sharpe.isna()
        valid_periods = [i for i, valid in enumerate(valid_mask) if valid]
        valid_sharpe = rolling_sharpe[valid_mask]
    
        ax3.plot(valid_periods, valid_sharpe, marker='o', linewidth=2, markersize=4, color='orange')
        ax3.axhline(y=0, color='red', linestyle='--', alpha=0.5)
        ax3.set_title(f'Rolling Sharpe Ratio ({rolling_sharpe_window}-period)', fontsize=14, fontweight='bold')
        ax3.set_xlabel('Period')
        ax3.set_ylabel('Sharpe Ratio')
        ax3.grid(True, alpha=0.3)
    
        # 4. Return Distribution (Bottom Right)
        bins = min(15, max(5, len(returns)//2))
        ax4.hist(returns, bins=bins, alpha=0.7, color='steelblue', edgecolor='black')
        mean_return = returns.mean()
        ax4.axvline(mean_return, color='red', linestyle='--', linewidth=2, 
                    label=f'Mean: {mean_return:.2f}%')
        ax4.set_title('Return Distribution', fontsize=14, fontweight='bold')
        ax4.set_xlabel('Return %')
        ax4.set_ylabel('Frequency')
        ax4.legend()
        ax4.grid(True, alpha=0.3)
    
        plt.tight_layout()
        plt.show()
    
    if __name__ == '__main__':
        # The end date is set to the current date for a more up-to-date backtest.
        current_date = datetime.now().date() 
    
        df = run_rolling_backtest(
            ticker="BTC-USD",
            start="2018-01-01",
            end=current_date, # Use the current date
            window_months=3,
    
        )
    
        print("\n=== ROLLING BACKTEST RESULTS ===")
        print(df)
    
        stats = report_stats(df)
        plot_four_charts(df)
Pasted image 20250622002006.png

6. Conclusion

The PivotPointStrategy offers a robust and adaptable approach to trading by integrating multi-timeframe pivot point analysis with essential volume and momentum confirmations. Its ability to identify both bounce and breakout opportunities, combined with a disciplined stop-loss mechanism, provides a comprehensive framework for navigating various market conditions. The use of rolling backtesting is crucial for validating the strategy’s consistency and effectiveness over different periods, offering more reliable insights than a single, long backtest. Further optimization of parameters (bounce/breakout thresholds, volume/RSI periods, stop loss) for specific assets and market regimes could further enhance its performance.