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.
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.
An entry is triggered when three key conditions align, indicating a powerful directional move:
All three conditions must be met simultaneously for an order to be placed.
The strategy employs a hierarchical approach to trade exits, prioritizing profit-taking and volatility-based exits over the trailing stop:
atr_tp_multiplier
* ATR),
the position is closed.atr_tp_multiplier
* ATR),
the position is closed.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.atr_stop_multiplier
times the current ATR.atr_stop_multiplier
times the current ATR. If the price
crosses this trailing stop, the position is closed.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 ---
= self.impulse_ema[0] > self.impulse_ema[-1]
ema_is_rising = self.macd_histo[0] > self.macd_histo[-1]
histo_is_rising = ema_is_rising and histo_is_rising
is_bullish_impulse = not ema_is_rising and not histo_is_rising
is_bearish_impulse = self.data.close[0] > self.trend_sma[0]
is_macro_uptrend = self.data.close[0] < self.trend_sma[0]
is_macro_downtrend = self.data.close[0] > self.bband.top[0]
is_bb_breakout_up = self.data.close[0] < self.bband.bot[0]
is_bb_breakout_down
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])
= self.highest_price_since_entry - (self.atr[0] * self.p.atr_stop_multiplier)
new_stop 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])
= self.lowest_price_since_entry + (self.atr[0] * self.p.atr_stop_multiplier)
new_stop self.stop_price = min(self.stop_price, new_stop)
if self.data.close[0] > self.stop_price: self.order = self.close()
params
)The strategy’s behavior is highly configurable through its parameters:
impulse_ema
: Period for the EMA used in the Elder
Impulse System.macd_fast
, macd_slow
,
macd_signal
: Parameters for the MACD Histogram.bb_period
, bb_devfactor
: Period and
standard deviation factor for Bollinger Bands.trend_period
: Period for the SMA used as the long-term
trend filter.vol_exit_period
, vol_exit_threshold
:
Parameters for the volatility collapse exit.atr_period
: Period for the Average True Range.atr_stop_multiplier
: Multiplier for ATR to set the
trailing stop distance.atr_tp_multiplier
: NEW Multiplier for
ATR to set the take profit target.__init__
)In the __init__
method, all necessary indicators are
instantiated:
self.impulse_ema
: EMA for the Elder Impulse
calculation.self.macd_histo
: MACD Histogram for the Elder Impulse
calculation.self.bband
: Bollinger Bands for breakout
detection.self.trend_sma
: Simple Moving Average for macro trend
filtering.self.atr
: Average True Range for both the trailing stop
and take profit target calculations.self.stddev
: Standard Deviation for the volatility
collapse exit.stop_price
,
take_profit_price
, highest_price_since_entry
,
and lowest_price_since_entry
are initialized to manage the
exit conditions.notify_order
)This method is crucial for handling order executions and managing the dynamic stop and take profit levels.
self.position
is active and
self.take_profit_price
is None
), the initial
take_profit_price
and stop_price
are
calculated based on the executed price, current ATR, and high/low of the
current bar.stop_price
,
take_profit_price
, highest_price_since_entry
,
lowest_price_since_entry
) are reset to None
,
preparing for the next potential trade.next
)The next
method, executed on each new bar, contains the
core strategy logic:
if not self.position
):
trend_sma
).buy
or
sell
order is placed.elif self.position
):
data.high[0]
reaches take_profit_price
, the
position is closed. For short positions, if data.low[0]
reaches take_profit_price
, the position is closed. A
return
statement immediately exits the next
method if TP is hit.self.stddev[0] / self.data.close[0]
) falls below
self.p.vol_exit_threshold
, indicating a significant drop in
volatility, the position is closed. This also uses a return
to prioritize.stop_price
continually moves up with
highest_price_since_entry
. For short positions,
stop_price
continually moves down with
lowest_price_since_entry
. If the price crosses the adjusted
stop_price
, the position is closed.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,=None
strategy_params
):= strategy_params or {}
strategy_params = []
all_results = pd.to_datetime(start)
start_dt = pd.to_datetime(end)
end_dt = start_dt
current_start
while True:
= current_start + rd.relativedelta(months=window_months)
current_end if current_end > end_dt:
= end_dt
current_end if current_start >= current_end:
break
print(f"\nROLLING BACKTEST: {current_start.date()} to {current_end.date()}")
= yf.download(ticker, start=current_start, end=current_end, auto_adjust=False, progress=False)
data
if data.empty or len(data) < 90:
print("Not enough data for this period. Skipping.")
+= rd.relativedelta(months=window_months)
current_start continue
if isinstance(data.columns, pd.MultiIndex):
= data.droplevel(1, 1)
data
= data['Close'].iloc[0]
start_price = data['Close'].iloc[-1]
end_price = (end_price - start_price) / start_price * 100
benchmark_ret
= bt.feeds.PandasData(dataname=data)
feed = bt.Cerebro()
cerebro
**strategy_params)
cerebro.addstrategy(strategy_class,
cerebro.adddata(feed)100000)
cerebro.broker.setcash(=0.001)
cerebro.broker.setcommission(commission=95)
cerebro.addsizer(bt.sizers.PercentSizer, percents
= cerebro.broker.getvalue()
start_val try:
cerebro.run()except Exception as e:
print(f"Error running backtest for {current_start.date()} to {current_end.date()}: {e}")
+= rd.relativedelta(months=window_months)
current_start continue
= cerebro.broker.getvalue()
final_val = (final_val - start_val) / start_val * 100
strategy_ret
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}%")
+= rd.relativedelta(months=window_months)
current_start
if current_start > end_dt:
break
return pd.DataFrame(all_results)
ticker
, start
,
end
: The asset symbol and the overall historical
period for the backtest.window_months
: The duration of each
individual backtesting window in months.strategy_class
: The
backtrader.Strategy
class to be tested (e.g.,
ElderImpulseBreakout
).strategy_params
: A dictionary to pass
specific parameters to the chosen strategy for each run.yfinance
with
auto_adjust=False
and droplevel
applied.backtrader.Cerebro
instance.strategy_class
and its
parameters.Pandas DataFrame
containing the
comprehensive results for each rolling window.
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.