← Back to Home
Navigating Market Regimes An Algorithmic Exploration of an Enhanced ATR Strategy

Navigating Market Regimes An Algorithmic Exploration of an Enhanced ATR Strategy

In the intricate world of financial markets, price movements are rarely uniform. Periods of strong, clear trends can give way to choppy, range-bound consolidation, or sudden bursts of extreme volatility. A rigid trading strategy, blind to these shifting market regimes, often struggles to maintain consistent performance. The challenge lies in building systems that not only identify a trend but also adapt to the prevailing market environment.

This article explores an algorithmic strategy that builds upon a foundational trend-following approach by integrating filters based on trend strength and volatility. It combines a simple moving average for trend identification with the Average True Range (ATR) for adaptive stop-losses and Average Directional Index (ADX) for trend strength confirmation. Furthermore, the exploration incorporates a walk-forward analysis framework, a critical tool for assessing a strategy’s robustness across changing market conditions.

The aim is to investigate whether layering these filters and employing dynamic risk management can enhance a basic trend-following system, potentially leading to more resilient performance by attempting to trade only when conditions are favorable and adapting risk to market volatility.


The Core Idea: Adaptive Trend Following with Regime Filters

This strategy investigates a refined hypothesis for trend trading, emphasizing the importance of market context:

  1. Basic Trend Identification (SMA): The strategy uses a Simple Moving Average (SMA) as its initial directional bias filter.
    • Hypothesis: When the closing price is consistently above its SMA, an uptrend is hypothesized. Conversely, a downtrend is suggested when the price is below its SMA. This is a common starting point for trend-following systems.
  2. Adaptive Risk Management (ATR Trailing Stop): ATR, a measure of market volatility, is employed to set dynamic stop-loss levels.
    • Hypothesis: Stop-loss distances should scale with the market’s natural “breathing room.” In volatile periods (high ATR), the stop will be wider to avoid premature exits due to noise. In calmer periods (low ATR), the stop will be tighter, reducing exposure. This aims to allow trades to capture larger trends while adapting to volatility.
  3. Trend Strength Confirmation (ADX Filter): The Average Directional Index (ADX) quantifies the strength of a trend, regardless of its direction.
    • Hypothesis: Trend-following strategies are most effective when a clear trend is present. By requiring the ADX to be above a certain adx_threshold (e.g., 20-25), the strategy attempts to trade only when the market is clearly trending, thus avoiding choppy, sideways markets where basic SMA crossovers often generate whipsaws and false signals. This is a crucial filter for a trend-following system.
  4. Volatility Threshold (ATR Filter): An additional filter based on ATR ensures that trades are only considered when there’s sufficient volatility, often expressed as a percentage of price.
    • Hypothesis: Trading in extremely low-volatility, range-bound markets might not offer enough price movement for profitable trend-following. By requiring ATR to be above a atr_threshold (e.g., 1.5% of price), the strategy seeks opportunities in markets that have enough “life.”
  5. Dynamic Exits (Filter Failure): Beyond the ATR trailing stop, the strategy also includes a mechanism to exit positions if the ADX or ATR filters cease to confirm favorable market conditions mid-trade.
    • Hypothesis: Market regimes can change. If a market ceases to trend or becomes too calm while a position is open, exiting promptly might mitigate losses or protect profits that could otherwise evaporate.
  6. Walk-Forward Analysis: A sophisticated backtesting technique employed to assess a strategy’s true robustness and adaptability.
    • Hypothesis: By systematically testing the strategy (often with parameters optimized on preceding data) on unseen out-of-sample periods, walk-forward analysis provides a more realistic estimate of a strategy’s future performance, guarding against overfitting to specific historical data.

The overarching strategy explores whether combining robust trend identification with adaptive risk management and intelligent market regime filtering can lead to a more resilient and potentially profitable trading system across diverse market environments.


Algorithmic Implementation: A backtrader Strategy

The following backtrader code provides a concrete implementation for exploring this Enhanced ATR Strategy. Each snippet will be presented and analyzed to understand how the theoretical ideas are translated into executable code.

Step 1: Initial Setup and Strategy Foundation

This section outlines the basic backtrader setup, including data loading and the EnhancedATRStrategy class’s initialization, where parameters and core indicators are defined.

import backtrader as bt
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
from datetime import datetime, timedelta # Used in walk-forward analysis
warnings.filterwarnings("ignore") # Suppress warnings, often from pandas/yfinance

%matplotlib inline
plt.rcParams['figure.figsize'] = (10, 8) # Larger default figure size

class EnhancedATRStrategy(bt.Strategy):
    params = (
        ('entry_sma_window', 90),    # SMA period for trend identification
        ('atr_window', 14),          # ATR period
        ('atr_multiplier', 1.0),     # ATR multiplier for trailing stop distance
        ('atr_threshold', 0.015),    # Minimum ATR as % of price for trading
        ('adx_period', 14),          # ADX period for trend strength
        ('adx_threshold', 30),       # Minimum ADX for trending market
        ('printlog', False),         # Flag to enable/disable detailed trade logs
    )
    
    def __init__(self):
        # Main indicators: SMA for trend, ATR for volatility/stops, ADX for trend strength
        self.sma = bt.indicators.SMA(period=self.params.entry_sma_window)
        self.atr = bt.indicators.ATR(period=self.params.atr_window)
        self.adx = bt.indicators.DirectionalMovementIndex(period=self.params.adx_period) # ADX includes +DI, -DI
        
        # Variables to store previous values of indicators for signal comparison
        self.prev_close = None
        self.prev_sma = None
        self.prev_atr = None
        
        # Position tracking variables
        self.trailing_stop = 0      # The current calculated price level of the trailing stop
        self.entry_price = 0        # Price at which the position was entered
        self.entry_date = None      # Date of entry (for logging, though not used in strategy logic)
        self.stop_order = None      # Reference to the active stop-loss order
        
        # backtrader's main order tracking variable
        self.order = None           # Reference to the main buy/sell order
        
        # Performance tracking (for internal logging, not final analysis)
        self.trade_count = 0        # Count of trades for logging

Analysis of this Snippet:

Step 2: Market Filters and Order Management

This section contains helper methods that check market regime filters (check_trend_filters), manage order status updates (notify_order), and handle the dynamic adjustment of the trailing stop (update_trailing_stop, check_stop_hit).

    def log(self, txt, dt=None):
        """Logging function to print messages if printlog is enabled."""
        if self.params.printlog:
            dt = dt or self.datas[0].datetime.date(0)
            print(f'{dt.isoformat()}: {txt}')

    def check_trend_filters(self):
        """Checks if current market conditions (ADX, ATR) allow trading."""
        # ADX filter: Market must be trending above a certain threshold
        if self.adx.adx[0] < self.params.adx_threshold:
            return False, "ADX too low (not trending)"
        
        # ATR filter: Current volatility must be sufficient (as a percentage of price)
        atr_pct = self.atr[0] / self.data.close[0]
        if atr_pct < self.params.atr_threshold:
            return False, f"ATR too low ({atr_pct:.3f} < {self.params.atr_threshold})"
        
        return True, "Filters passed" # Both filters cleared

    def notify_order(self, order):
        """Manages order status updates, logs trade executions, and sets initial trailing stop."""
        # Ignore orders that are just submitted or accepted, wait for completion or failure
        if order.status in [order.Submitted, order.Accepted]:
            return

        # If order has completed (filled)
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(f'BUY EXECUTED: Price: {order.executed.price:.4f}')
                # If a new long position is opened (size > 0), set the initial trailing stop
                if self.position.size > 0:
                    self.entry_price = order.executed.price
                    self.entry_date = self.datas[0].datetime.date(0) # Record entry date
                    # Initial stop is entry price minus ATR_multiplier * previous ATR
                    self.trailing_stop = self.entry_price - (self.params.atr_multiplier * self.prev_atr)
                    self.trade_count += 1 # Increment trade count
                    self.log(f'LONG ENTRY #{self.trade_count}: Stop at {self.trailing_stop:.4f}')
            
            elif order.issell(): # Could be for short entry or closing a long
                self.log(f'SELL EXECUTED: Price: {order.executed.price:.4f}')
                # If a new short position is opened (size < 0), set the initial trailing stop
                if self.position.size < 0:
                    self.entry_price = order.executed.price
                    self.entry_date = self.datas[0].datetime.date(0)
                    # Initial stop is entry price plus ATR_multiplier * previous ATR
                    self.trailing_stop = self.entry_price + (self.params.atr_multiplier * self.prev_atr)
                    self.trade_count += 1
                    self.log(f'SHORT ENTRY #{self.trade_count}: Stop at {self.trailing_stop:.4f}')
                
                elif self.position.size == 0: # If it was a closing order (long closed or short covered)
                    self.entry_date = None # Clear entry date
                    self.log(f'POSITION CLOSED')

        # If order failed (canceled, margin call, rejected)
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log(f'Order Failed: {order.status}')

        self.order = None # Clear the main order reference once it's processed

    def update_trailing_stop(self):
        """Updates the trailing stop level based on current price and ATR."""
        if not self.position: # Only update if a position is open
            return
        
        current_price = self.data.close[0]
        current_atr = self.atr[0]
        
        if self.position.size > 0: # Long position: trail stop up (never down)
            new_stop = current_price - (self.params.atr_multiplier * current_atr)
            if new_stop > self.trailing_stop: # Only update if the new stop is higher (in profit direction)
                self.trailing_stop = new_stop
                self.log(f'LONG TRAILING STOP UPDATED: {self.trailing_stop:.4f}')
        
        elif self.position.size < 0: # Short position: trail stop down (never up)
            new_stop = current_price + (self.params.atr_multiplier * current_atr)
            if new_stop < self.trailing_stop: # Only update if the new stop is lower (in profit direction)
                self.trailing_stop = new_stop
                self.log(f'SHORT TRAILING STOP UPDATED: {self.trailing_stop:.4f}')

    def check_stop_hit(self):
        """Checks if the trailing stop has been hit by the current bar's price action."""
        if not self.position: # No position, no stop to check
            return False
        
        if self.position.size > 0: # Long position
            # If current low breaks below trailing stop, stop is hit
            if self.data.low[0] <= self.trailing_stop:
                self.log(f'LONG STOP HIT: Low {self.data.low[0]:.4f} <= Stop {self.trailing_stop:.4f}')
                return True
        
        elif self.position.size < 0: # Short position
            # If current high breaks above trailing stop, stop is hit
            if self.data.high[0] >= self.trailing_stop:
                self.log(f'SHORT STOP HIT: High {self.data.high[0]:.4f} >= Stop {self.trailing_stop:.4f}')
                return True
        
        return False # Stop not hit

Analysis of Market Filters and Order Management:

Step 3: The Bar-by-Bar Trading Logic

This section contains the next method, the main execution loop for each bar. It orchestrates the strategy’s flow, combining trend signals, market regime filters, and adaptive exits.

    def next(self):
        # Store previous values of indicators. This is done early for comparison in current bar's logic.
        if len(self.data) > 1: # Ensure a previous bar exists
            self.prev_close = self.data.close[-1]
            self.prev_sma = self.sma[-1]
            self.prev_atr = self.atr[-1]
        
        # If an order is pending completion, do nothing this bar to avoid conflicting commands.
        if self.order:
            return
        
        # Skip if indicators haven't warmed up or have NaN values (common at the start of data)
        if (self.prev_close is None or 
            np.isnan(self.prev_sma) or 
            np.isnan(self.prev_atr) or
            np.isnan(self.adx.adx[0])): # Also check ADX for NaN
            return
        
        # First priority: Check if a trailing stop has been hit for an open position.
        # This handles profit-taking or loss-cutting.
        if self.check_stop_hit():
            self.order = self.close() # Close the position immediately
            return
        
        # If stop wasn't hit, update its level for the current bar's new price
        self.update_trailing_stop()
        
        # Check market regime filters (ADX and ATR thresholds)
        filters_ok, filter_info = self.check_trend_filters()
        
        if not filters_ok:
            # If filters fail and we have a position, it's a "filter exit"
            if self.position:
                self.log(f'FILTER EXIT: {filter_info}')
                self.order = self.close() # Close the position due to unfavorable market conditions
            return # Do not consider new entries if filters fail
        
        # --- Entry/Reversal Logic (only executed if filters pass) ---
        # This part determines whether to open a new position or reverse an existing one.
        
        # Long signal: Previous close was above Previous SMA (indicating an uptrend)
        if self.prev_close > self.prev_sma:
            if self.position.size < 0: # If currently short, close short and prepare to reverse to long
                self.log(f'REVERSAL: Short to Long (ADX: {self.adx.adx[0]:.1f})')
                self.order = self.close() # Close current short position
                # A new long order will be placed on the next bar once this closing order completes.
            elif self.position.size == 0: # If currently flat, go long
                self.log(f'LONG SIGNAL (ADX: {self.adx.adx[0]:.1f})')
                self.order = self.buy() # Place a buy order
        
        # Short signal: Previous close was below Previous SMA (indicating a downtrend)
        elif self.prev_close < self.prev_sma:
            if self.position.size > 0: # If currently long, close long and prepare to reverse to short
                self.log(f'REVERSAL: Long to Short (ADX: {self.adx.adx[0]:.1f})')
                self.order = self.close() # Close current long position
                # A new short order will be placed on the next bar once this closing order completes.
            elif self.position.size == 0: # If currently flat, go short
                self.log(f'SHORT SIGNAL (ADX: {self.adx.adx[0]:.1f})')
                self.order = self.sell() # Place a sell order

    def stop(self):
        """Called at the end of the backtest. Logs final summary."""
        self.log(f'Total Trades: {self.trade_count}')
        self.log(f'Final Portfolio Value: {self.broker.getvalue():.2f}')
        
        # Calculate basic total return (initial cash of 100000 assumed)
        initial_cash = 100000
        total_return = (self.broker.getvalue() - initial_cash) / initial_cash * 100
        self.log(f'Total Return: {total_return:.2f}%')

Analysis of the Bar-by-Bar Trading Logic:

Step 4: The Research Framework: Backtest & Walk-Forward Analysis

This section outlines the setup for running both a single, comprehensive backtest and a more rigorous walk-forward analysis, providing a deeper assessment of the strategy’s robustness.

# --- Parameters ---
ticker = "BTC-USD" # Asset for backtesting
start_date = "2020-01-01"
end_date = "2024-12-31"

print(f"=== ENHANCED ATR STRATEGY WITH FILTERS (SIMPLIFIED) ===")
print(f"Asset: {ticker}")
print(f"Period: {start_date} to {end_date}")
print("Enhancements:")
print("✓ ADX trend filter (>25)") # Note: This prints the description, actual value is in cerebro.addstrategy
print("✓ ATR volatility filter (>1.5%)") # Note: This prints the description, actual value is in cerebro.addstrategy
print("=" * 50)
    
# Download full dataset for main backtest
print("\nDownloading data for main backtest...")
data = yf.download(ticker, start=start_date, end=end_date, progress=False)
if hasattr(data.columns, 'levels'):
    data.columns = data.columns.droplevel(1) # Handle multi-level columns from yfinance
    
print(f"Data downloaded. Shape: {data.shape}")
    
# --- Run main backtest ---
print("\n=== MAIN BACKTEST ===")
cerebro = bt.Cerebro() # Initialize cerebro engine
    
cerebro.addstrategy(EnhancedATRStrategy,
                    entry_sma_window=100, # Override default params for this run
                    atr_window=14,
                    atr_multiplier=3.0,
                    atr_threshold=0.015,   # 1.5% minimum ATR
                    adx_threshold=25,      # ADX > 25 required for trending
                    printlog=False)
    
cerebro.adddata(bt.feeds.PandasData(dataname=data)) # Add data feed
cerebro.broker.setcash(100000.0) # Set initial capital
cerebro.broker.setcommission(commission=0.001) # Set commission (0.1%)
cerebro.addsizer(bt.sizers.PercentSizer, percents=95) # Allocate 95% of capital per trade
    
# Add analyzers for comprehensive performance analysis
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades')
    
print(f'Starting Portfolio Value: ${cerebro.broker.getvalue():,.2f}')
    
results = cerebro.run() # Execute the backtest
strat = results[0] # Get the strategy instance from the results

print(f'Final Portfolio Value: ${cerebro.broker.getvalue():,.2f}')
    
# Print comprehensive performance metrics
print('\n--- PERFORMANCE METRICS ---')
sharpe_analysis = strat.analyzers.sharpe.get_analysis()
sharpe_ratio = sharpe_analysis.get('sharperatio', None)
print(f'Sharpe Ratio: {sharpe_ratio:.3f}' if sharpe_ratio else 'Sharpe Ratio: N/A')
    
drawdown_analysis = strat.analyzers.drawdown.get_analysis()
max_drawdown = drawdown_analysis.get('max', {}).get('drawdown', 0)
print(f'Max Drawdown: {max_drawdown:.2f}%')
    
returns_analysis = strat.analyzers.returns.get_analysis()
total_return = returns_analysis.get('rtot', 0) * 100
print(f'Total Return: {total_return:.2f}%')
    
trade_analysis = strat.analyzers.trades.get_analysis()
total_trades = trade_analysis.get('total', {}).get('total', 0)
won_trades = trade_analysis.get('won', {}).get('total', 0)
win_rate = (won_trades / total_trades * 100) if total_trades > 0 else 0
print(f'Total Trades: {total_trades}')
print(f'Win Rate: {win_rate:.1f}%')
    
if total_trades > 0:
    avg_win = trade_analysis.get('won', {}).get('pnl', {}).get('average', 0)
    avg_loss = trade_analysis.get('lost', {}).get('pnl', {}).get('average', 0)
    print(f'Average Win: ${avg_win:.2f}')
    print(f'Average Loss: ${avg_loss:.2f}')
        
    if avg_loss != 0:
        profit_factor = abs(trade_analysis.get('won', {}).get('pnl', {}).get('total', 0) / trade_analysis.get('lost', {}).get('pnl', {}).get('total', 0))
        print(f'Profit Factor: {profit_factor:.2f}')

print('\n--- COMPARISON WITH BUY & HOLD ---')
# Calculate buy & hold return for the full period
bh_return = ((data['Close'][-1] / data['Close'][0]) - 1) * 100
print(f'Buy & Hold Return: {bh_return:.2f}%')
print(f'Strategy vs B&H: {total_return - bh_return:.2f}% outperformance')
    
# Plot results of the main backtest
cerebro.plot(iplot=False, style='line', volume=False)
plt.suptitle(f'Enhanced ATR Strategy (Simplified) - {ticker}', fontsize=16)
plt.tight_layout()
plt.show()
    
# --- Walk-forward analysis execution ---
def run_walk_forward_analysis(ticker, start_date, end_date, window_months=12, step_months=6):
    """
    Performs a simplified walk-forward analysis by repeatedly backtesting
    on out-of-sample periods using a fixed set of parameters.
    """
    print("=== WALK-FORWARD ANALYSIS ===\n")
    
    start = pd.to_datetime(start_date)
    end = pd.to_datetime(end_date)
    
    results = []
    current_start = start
    
    while current_start < end:
        # Define in-sample and out-of-sample periods
        insample_end = current_start + pd.DateOffset(months=window_months)
        outsample_start = insample_end # Out-sample starts immediately after in-sample
        outsample_end = outsample_start + pd.DateOffset(months=step_months) # Out-sample duration
        
        # Break if out-sample period extends beyond overall end date
        if outsample_end > end:
            break
        
        print(f"In-sample Period: {current_start.date()} to {insample_end.date()}")
        print(f"Out-sample Period: {outsample_start.date()} to {outsample_end.date()}")
        
        # Download data for the current out-of-sample period
        oos_data = yf.download(ticker, start=outsample_start, end=outsample_end, progress=False)
        if hasattr(oos_data.columns, 'levels'):
            oos_data.columns = oos_data.columns.droplevel(1)
        
        # Check if enough data is available for indicator warm-up
        min_required_days = 120 # Roughly SMA period + ADX period
        if len(oos_data) < min_required_days:
            print(f"Insufficient data for out-sample: {len(oos_data)} days < {min_required_days} required. Skipping.\n")
            current_start = outsample_start # Move window forward even if skipped
            continue
        
        try:
            # Run strategy on the out-of-sample period with a predefined parameter set
            cerebro = bt.Cerebro()
            cerebro.addstrategy(EnhancedATRStrategy, 
                                entry_sma_window=50, # Parameters are fixed for this walk-forward
                                atr_window=14,
                                atr_multiplier=3.0,
                                atr_threshold=0.01,  # Slightly lower ATR threshold for more trades
                                adx_threshold=20,    # Slightly lower ADX threshold for more trades
                                printlog=False)
            cerebro.adddata(bt.feeds.PandasData(dataname=oos_data))
            cerebro.broker.setcash(100000.0)
            cerebro.addsizer(bt.sizers.PercentSizer, percents=95)
            
            result = cerebro.run() # Run the backtest for this period
            final_value = cerebro.broker.getvalue()
            oos_return = (final_value - 100000) / 100000 * 100 # Calculate return for this period
            
            # Calculate Buy & Hold return for comparison in this out-sample period
            bh_return = ((oos_data['Close'][-1] / oos_data['Close'][0]) - 1) * 100
            
            results.append({
                'period': f"{outsample_start.date()} to {outsample_end.date()}",
                'strategy_return': oos_return,
                'buyhold_return': bh_return,
                'outperformance': oos_return - bh_return
            })
            
            print(f"Strategy Return: {oos_return:.2f}%")
            print(f"Buy & Hold: {bh_return:.2f}%")
            print(f"Outperformance: {oos_return - bh_return:.2f}%\n")
            
        except Exception as e:
            print(f"Error in backtest for period {outsample_start.date()} to {outsample_end.date()}: {str(e)}")
            print("Skipping this period...\n")
        
        current_start = outsample_start # Move the window forward for the next iteration
    
    # Summarize walk-forward results
    if results:
        avg_strategy = np.mean([r['strategy_return'] for r in results])
        avg_bh = np.mean([r['buyhold_return'] for r in results])
        avg_outperform = np.mean([r['outperformance'] for r in results])
        win_rate = sum(1 for r in results if r['outperformance'] > 0) / len(results) * 100 # % periods outperforming B&H
        
        print("=== WALK-FORWARD SUMMARY ===")
        print(f"Average Strategy Return (per period): {avg_strategy:.2f}%")
        print(f"Average Buy & Hold Return (per period): {avg_bh:.2f}%")
        print(f"Average Outperformance (per period): {avg_outperform:.2f}%")
        print(f"Outperformance Win Rate (vs B&H): {win_rate:.1f}%")
        print(f"Number of periods evaluated: {len(results)}")
    else:
        print("No valid periods found for walk-forward analysis")
        
    return results

# Main execution block
if __name__ == "__main__":
    # Parameters for the main backtest and walk-forward analysis
    ticker = "BTC-USD"
    start_date = "2020-01-01"
    end_date = "2024-12-31" # Up to current time

    print(f"=== ENHANCED ATR STRATEGY WITH FILTERS (SIMPLIFIED) ===")
    print(f"Asset: {ticker}")
    print(f"Period: {start_date} to {end_date}")
    print("Enhancements:")
    print("✓ ADX trend filter (>25)") # Describes the intent, parameter set in cerebro.addstrategy
    print("✓ ATR volatility filter (>1.5%)") # Describes the intent
    print("=" * 50)
    
    # Download full dataset for the main backtest run
    print("\nDownloading data for main backtest...")
    data = yf.download(ticker, start=start_date, end=end_date, progress=False)
    if hasattr(data.columns, 'levels'):
        data.columns = data.columns.droplevel(1) # Flatten columns if multi-level
    
    print(f"Data downloaded. Shape: {data.shape}")
    
    # --- Run the full period main backtest ---
    print("\n=== MAIN BACKTEST ===")
    cerebro = bt.Cerebro() # Initialize cerebro engine
    
    cerebro.addstrategy(EnhancedATRStrategy,
                        entry_sma_window=100, # Parameters for this full backtest
                        atr_window=14,
                        atr_multiplier=3.0,
                        atr_threshold=0.015, # 1.5% minimum ATR
                        adx_threshold=25,    # ADX > 25 required
                        printlog=False)
    
    cerebro.adddata(bt.feeds.PandasData(dataname=data)) # Add data feed
    cerebro.broker.setcash(100000.0) # Set initial capital
    cerebro.broker.setcommission(commission=0.001) # Set commission (0.1%)
    cerebro.addsizer(bt.sizers.PercentSizer, percents=95) # Allocate 95% of capital per trade
    
    # Add analyzers for comprehensive performance analysis (for main backtest)
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
    cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
    cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
    cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades')
    
    print(f'Starting Portfolio Value: ${cerebro.broker.getvalue():,.2f}')
    
    results = cerebro.run() # Execute the main backtest
    strat = results[0] # Get the strategy instance from the results list
    
    print(f'Final Portfolio Value: ${cerebro.broker.getvalue():,.2f}')
    
    # Print comprehensive performance metrics for the main backtest
    print('\n--- PERFORMANCE METRICS (MAIN BACKTEST) ---')
    sharpe_analysis = strat.analyzers.sharpe.get_analysis()
    sharpe_ratio = sharpe_analysis.get('sharperatio', None)
    print(f'Sharpe Ratio: {sharpe_ratio:.3f}' if sharpe_ratio else 'Sharpe Ratio: N/A')
    
    drawdown_analysis = strat.analyzers.drawdown.get_analysis()
    max_drawdown = drawdown_analysis.get('max', {}).get('drawdown', 0)
    print(f'Max Drawdown: {max_drawdown:.2f}%')
    
    returns_analysis = strat.analyzers.returns.get_analysis()
    total_return = returns_analysis.get('rtot', 0) * 100
    print(f'Total Return: {total_return:.2f}%')
    
    trade_analysis = strat.analyzers.trades.get_analysis()
    total_trades = trade_analysis.get('total', {}).get('total', 0)
    won_trades = trade_analysis.get('won', {}).get('total', 0)
    win_rate = (won_trades / total_trades * 100) if total_trades > 0 else 0
    print(f'Total Trades: {total_trades}')
    print(f'Win Rate: {win_rate:.1f}%')
    
    if total_trades > 0:
        avg_win = trade_analysis.get('won', {}).get('pnl', {}).get('average', 0)
        avg_loss = trade_analysis.get('lost', {}).get('pnl', {}).get('average', 0)
        print(f'Average Win: ${avg_win:.2f}')
        print(f'Average Loss: ${avg_loss:.2f}')
        
        if avg_loss != 0:
            profit_factor = abs(trade_analysis.get('won', {}).get('pnl', {}).get('total', 0) / trade_analysis.get('lost', {}).get('pnl', {}).get('total', 0))
            print(f'Profit Factor: {profit_factor:.2f}')

    # Buy & Hold comparison for the main backtest
    bh_return = ((data['Close'][-1] / data['Close'][0]) - 1) * 100
    print(f'Buy & Hold Return: {bh_return:.2f}%')
    print(f'Strategy vs B&H: {total_return - bh_return:.2f}% outperformance')
    
    # Plot results of the main backtest
    cerebro.plot(iplot=False, style='line', volume=False)
    plt.suptitle(f'Enhanced ATR Strategy (Simplified) - {ticker}', fontsize=16)
    plt.tight_layout()
    plt.show()
    
    # --- Execute Walk-Forward Analysis ---
    print("\n" + "="*50)
    wf_results = run_walk_forward_analysis(ticker, "2021-01-01", "2024-12-31", 
                                             window_months=12, step_months=6)
Pasted image 20250613224036.png

Analysis of the Backtest Execution and Walk-Forward Analysis:


Reflections on Our Enhanced ATR Strategy Expedition

This backtrader strategy represents a robust and well-thought-out exploration into resilient trend-following, particularly distinguished by its sophisticated filtering and the inclusion of a walk-forward analysis framework.

This strategy serves as an excellent, well-engineered foundation for further research into resilient algorithmic trading. It embodies a sophisticated approach to market regime awareness and adaptive risk management, making it a compelling candidate for exploring robust performance across varying market conditions.