← Back to Home
The Elder Impulse Breakout Trading Strategy

The Elder Impulse Breakout Trading Strategy

The Elder Impulse System, developed by Dr. Alexander Elder, is a trading approach that combines the Exponential Moving Average (EMA) and the MACD Histogram to gauge market momentum and trend strength. When both indicators agree on the direction, the “Impulse” is considered to be strong. This article introduces the ElderImpulseBreakout strategy, which seeks to capitalize on these strong impulse periods when they coincide with a Bollinger Band breakout, filtered by a broader market trend. It also incorporates sophisticated exit conditions, including a volatility collapse exit and a profit target, in addition to an ATR-based trailing stop.

Strategy Overview

The ElderImpulseBreakout strategy aims for trend-following entries during periods of strong market momentum, confirmed by a price breakout from Bollinger Bands. It uses a hierarchy of exit conditions to manage trades effectively.

Entry Logic

An entry is triggered when three key conditions align, indicating a powerful directional move:

  1. Elder Impulse System Confirmation:
    • Bullish Impulse: The short-term EMA is rising (current value > previous value) AND the MACD Histogram is rising (current value > previous value). This suggests strong bullish momentum.
    • Bearish Impulse: The short-term EMA is falling (current value < previous value) AND the MACD Histogram is falling (current value < previous value). This suggests strong bearish momentum.
  2. Macro Trend Filter: The price must be in alignment with a longer-term trend.
    • For long entries, the current close price must be above a long-term Simple Moving Average (SMA).
    • For short entries, the current close price must be below a long-term SMA.
  3. Bollinger Band Breakout: The price must break out of its Bollinger Bands, indicating significant volatility and a potential continuation of the newly established momentum.
    • For long entries, the current close price must be above the upper Bollinger Band.
    • For short entries, the current close price must be below the lower Bollinger Band.

All three conditions must be met simultaneously for an order to be placed.

Exit Logic

The strategy employs a hierarchical approach to trade exits, prioritizing profit-taking and volatility-based exits over the trailing stop:

  1. Take Profit (TP): This is the highest priority exit.
    • For long positions, if the high price reaches a predefined target (entry price + atr_tp_multiplier * ATR), the position is closed.
    • For short positions, if the low price reaches a predefined target (entry price - atr_tp_multiplier * ATR), the position is closed.
  2. Volatility Collapse Exit: If the market’s volatility, measured by the stddev (Standard Deviation) relative to the close price, drops below a certain vol_exit_threshold, the position is closed. This helps exit trades when momentum wanes and the market becomes choppy.
  3. ATR Trailing Stop: This acts as a dynamic stop-loss.
    • For long positions, the stop price moves up as the highest price reached since entry increases, trailing behind by atr_stop_multiplier times the current ATR.
    • For short positions, the stop price moves down as the lowest price reached since entry decreases, trailing ahead by atr_stop_multiplier times the current ATR. If the price crosses this trailing stop, the position is closed.

Backtrader Implementation

The strategy is structured within backtrader as follows:

import backtrader as bt

class ElderImpulseBreakout(bt.Strategy):
   
    params = (
        # Impulse System
        ('impulse_ema', 7),
        ('macd_fast', 7), ('macd_slow', 30), ('macd_signal', 7),
        # Breakout
        ('bb_period', 7), ('bb_devfactor', 2.0),
        # Trend Filter
        ('trend_period', 30),
        # Volatility Collapse Exit
        ('vol_exit_period', 7),
        ('vol_exit_threshold', 0.01),
        # Risk Management
        ('atr_period', 7),
        ('atr_stop_multiplier', 3.0),
        ('atr_tp_multiplier', 5.0), # NEW: Take Profit at 4x ATR
    )

    def __init__(self):
        self.order = None
        self.impulse_ema = bt.indicators.ExponentialMovingAverage(self.data, period=self.p.impulse_ema)
        self.macd_histo = bt.indicators.MACDHistogram(self.data, period_me1=self.p.macd_fast, period_me2=self.p.macd_slow, period_signal=self.p.macd_signal)
        self.bband = bt.indicators.BollingerBands(self.data, period=self.p.bb_period, devfactor=self.p.bb_devfactor)
        self.trend_sma = bt.indicators.SimpleMovingAverage(self.data, period=self.p.trend_period)
        self.atr = bt.indicators.AverageTrueRange(self.data, period=self.p.atr_period)
        self.stddev = bt.indicators.StandardDeviation(self.data, period=self.p.vol_exit_period)

        # --- State Variables ---
        self.stop_price = None
        self.take_profit_price = None
        self.highest_price_since_entry = None
        self.lowest_price_since_entry = None

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]: return
        if order.status in [order.Completed]:
            # If a new position was just opened, initialize the exit prices
            if order.isbuy() and self.position.size > 0 and self.take_profit_price is None:
                self.take_profit_price = order.executed.price + (self.atr[0] * self.p.atr_tp_multiplier)
                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() and self.position.size < 0 and self.take_profit_price is None:
                self.take_profit_price = order.executed.price - (self.atr[0] * self.p.atr_tp_multiplier)
                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 a position was closed, reset all state
            elif not self.position:
                self.stop_price = None; self.take_profit_price = None
                self.highest_price_since_entry = None; self.lowest_price_since_entry = None
        self.order = None

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

        if not self.position:
            # --- Entry Logic ---
            ema_is_rising = self.impulse_ema[0] > self.impulse_ema[-1]
            histo_is_rising = self.macd_histo[0] > self.macd_histo[-1]
            is_bullish_impulse = ema_is_rising and histo_is_rising
            is_bearish_impulse = not ema_is_rising and not histo_is_rising
            is_macro_uptrend = self.data.close[0] > self.trend_sma[0]
            is_macro_downtrend = self.data.close[0] < self.trend_sma[0]
            is_bb_breakout_up = self.data.close[0] > self.bband.top[0]
            is_bb_breakout_down = self.data.close[0] < self.bband.bot[0]
            
            if is_bullish_impulse and is_macro_uptrend and is_bb_breakout_up:
                self.order = self.buy()
            elif is_bearish_impulse and is_macro_downtrend and is_bb_breakout_down:
                self.order = self.sell()
        
        elif self.position:
            # --- Exit Logic Hierarchy ---
            if self.position.size > 0: # Long Position
                # 1. Check for Take Profit
                if self.data.high[0] >= self.take_profit_price:
                    self.order = self.close()
                    return
                # 2. Check for Volatility Collapse
                if (self.stddev[0] / self.data.close[0]) < self.p.vol_exit_threshold:
                    self.order = self.close()
                    return
                # 3. Manage ATR Trailing Stop
                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)
                if self.data.close[0] < self.stop_price: self.order = self.close()

            elif self.position.size < 0: # Short Position
                # 1. Check for Take Profit
                if self.data.low[0] <= self.take_profit_price:
                    self.order = self.close()
                    return
                # 2. Check for Volatility Collapse
                if (self.stddev[0] / self.data.close[0]) < self.p.vol_exit_threshold:
                    self.order = self.close()
                    return
                # 3. Manage ATR Trailing Stop
                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)
                if self.data.close[0] > self.stop_price: self.order = self.close()

Parameters (params)

The strategy’s behavior is highly configurable through its parameters:

Initialization (__init__)

In the __init__ method, all necessary indicators are instantiated:

Order Notification (notify_order)

This method is crucial for handling order executions and managing the dynamic stop and take profit levels.

Main Logic (next)

The next method, executed on each new bar, contains the core strategy logic:

Rolling Backtesting Setup

To evaluate the strategy’s performance, we’ll employ a rolling backtest. This method assesses the strategy over multiple, successive time windows, providing a more robust view of its consistency compared to a single, fixed backtest.

from collections import deque
import backtrader as bt
import numpy as np
import pandas as pd
import yfinance as yf
import dateutil.relativedelta as rd

# Assuming ElderImpulseBreakout class is defined above this section.

def run_rolling_backtest(
    ticker,
    start,
    end,
    window_months,
    strategy_class,
    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 = yf.download(ticker, start=current_start, end=current_end, auto_adjust=False, progress=False)

        if data.empty or len(data) < 90:
            print("Not enough data for this period. Skipping.")
            current_start += rd.relativedelta(months=window_months)
            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_ret = (end_price - start_price) / start_price * 100

        feed = bt.feeds.PandasData(dataname=data)
        cerebro = bt.Cerebro()
        
        cerebro.addstrategy(strategy_class, **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()
        try:
            cerebro.run()
        except Exception as e:
            print(f"Error running backtest for {current_start.date()} to {current_end.date()}: {e}")
            current_start += rd.relativedelta(months=window_months)
            continue

        final_val = cerebro.broker.getvalue()
        strategy_ret = (final_val - start_val) / start_val * 100

        all_results.append({
            'start': current_start.date(),
            'end': current_end.date(),
            'strategy_return_pct': strategy_ret,
            'benchmark_return_pct': benchmark_ret,
            'final_value': final_val,
        })

        print(f"Strategy Return: {strategy_ret:.2f}% | Buy & Hold Return: {benchmark_ret:.2f}%")

        current_start += rd.relativedelta(months=window_months)

        if current_start > end_dt:
            break

    return pd.DataFrame(all_results)

How the Rolling Backtest Works:

Pasted image 20250720155035.png Pasted image 20250720155040.png Pasted image 20250720155114.png

Conclusion

The ElderImpulseBreakout strategy is a sophisticated approach to momentum trading, combining multiple technical analysis concepts to identify high-probability setups. By integrating Elder’s Impulse System for momentum, Bollinger Bands for breakout confirmation, a long-term SMA for trend filtering, and a robust multi-layered exit strategy (take profit, volatility collapse, and ATR trailing stop), it aims to capture significant moves while diligently managing risk. The use of a rolling backtest provides a more thorough and reliable evaluation of its performance across diverse market conditions, offering insights into its consistency and adaptability. As with any algorithmic trading strategy, careful analysis, optimization, and continuous monitoring are essential.