This tutorial uses MACDADXConfluenceStrategy
from the Mega
Backtrader Strategy Pack. The package includes the complete strategy
source and the backtest runner used to produce these
results.
The MACD–ADX Confluence strategy returned 20.04% on XRP-USD while XRP buy-and-hold lost 67.22%. It produced a 1.64 Sharpe ratio and a 9.43% maximum drawdown.
Each backtest started with $10,000 and used 1-hour bars from August 14, 2025 at 15:00 UTC through August 14, 2026 at 15:00 UTC, 0.10% commission per execution, 95% position sizing, and long-only execution. Every run ended with zero open trades.
The strategy waits for a MACD crossover, confirms trend strength and direction with ADX and directional indicators, requires above-average volume, and manages each position with a two-ATR trailing stop.
Install the minimal dependencies:
pip install backtrader yfinance pandas matplotlib numpyThen fetch and normalize the hourly OHLCV frame:
from datetime import datetime, timezone
import backtrader as bt
import pandas as pd
import yfinance as yf
symbol = "XRP-USD"
data = yf.download(
symbol,
start=datetime(2025, 8, 14, 15, tzinfo=timezone.utc),
end=datetime(2026, 8, 14, 16, tzinfo=timezone.utc),
interval="1h",
auto_adjust=False,
progress=False,
)
if isinstance(data.columns, pd.MultiIndex):
data.columns = data.columns.get_level_values(0)
data = data[["Open", "High", "Low", "Close", "Volume"]].dropna()Each input handles a different part of the setup: momentum, trend strength, participation, or risk.
params = (
('macd_fast', 12), ('macd_slow', 26), ('macd_signal', 9),
('adx_period', 14), ('adx_threshold', 25),
('volume_period', 20),
('atr_period', 14), ('atr_multiplier', 2.0),
)MACD identifies the crossover, while ADX, +DI, and -DI describe trend strength and direction.
self.macd = bt.indicators.MACD(
self.data,
period_me1=self.p.macd_fast,
period_me2=self.p.macd_slow,
period_signal=self.p.macd_signal,
)
self.adx = bt.indicators.ADX(self.data, period=self.p.adx_period)
self.plusdi = bt.indicators.PlusDI(self.data, period=self.p.adx_period)
self.minusdi = bt.indicators.MinusDI(self.data, period=self.p.adx_period)The current MACD line must move above its signal after being at or below it on the previous bar.
macd_bullish = (
self.macd.macd[0] > self.macd.signal[0]
and self.macd.macd[-1] <= self.macd.signal[-1]
)ADX must exceed 25, +DI must exceed -DI, and current volume must be above its 20-hour average.
trend_is_strong = self.adx[0] >= self.p.adx_threshold
direction_is_bullish = self.plusdi[0] > self.minusdi[0]
volume_ok = self.data.volume[0] > self.volume_sma[0]No single indicator can trigger the trade by itself.
long_signal = (
macd_bullish
and trend_is_strong
and direction_is_bullish
and volume_ok
)
if not self.position and long_signal:
self.order = self.buy()The stop distance expands and contracts with current market volatility.
self.trail_order = self.sell(
exectype=bt.Order.StopTrail,
trailamount=self.atr[0] * self.p.atr_multiplier,
)Replace StrategyClass with the class built above. This
is the minimal engine configuration: $10,000 starting cash, 0.10%
commission, and 95% position sizing.
cerebro = bt.Cerebro()
cerebro.broker.setcash(10_000)
cerebro.broker.setcommission(commission=0.001) # 0.10% per execution
cerebro.addsizer(bt.sizers.PercentSizer, percents=95)
cerebro.adddata(bt.feeds.PandasData(dataname=data))
cerebro.addstrategy(StrategyClass)
result = cerebro.run()[0]
print(f"Final value: ${cerebro.broker.getvalue():,.2f}")The following script includes the complete strategy, data download,
long-only execution rule, analyzers, and printed results. Save it as a
.py file and run it directly.
import math
from datetime import datetime, timezone
import backtrader as bt
import backtrader.indicators as btind
import numpy as np
import pandas as pd
import yfinance as yf
# Complete strategy implementation
class MACDADXConfluenceStrategy(bt.Strategy):
params = (
('macd_fast', 12), # MACD fast period
('macd_slow', 26), # MACD slow period
('macd_signal', 9), # MACD signal period
('adx_period', 14), # ADX period
('adx_threshold', 25), # ADX threshold for trend strength
('volume_period', 20), # Period for volume average
('atr_period', 14), # ATR period for trailing stops
('atr_multiplier', 2.0), # ATR multiplier for trailing stops
('printlog', True),
)
def log(self, txt, dt=None, doprint=False):
if self.params.printlog or doprint:
dt = dt or self.datas[0].datetime.datetime(0)
print(f"{dt.isoformat()} - {txt}")
def __init__(self):
self.dataclose = self.datas[0].close
self.datavolume = self.datas[0].volume
# MACD indicator
self.macd = bt.indicators.MACD(self.datas[0],
period_me1=self.params.macd_fast,
period_me2=self.params.macd_slow,
period_signal=self.params.macd_signal)
# ADX and directional movement indicators
self.adx = bt.indicators.ADX(self.datas[0], period=self.params.adx_period)
self.plusdi = bt.indicators.PlusDI(self.datas[0], period=self.params.adx_period)
self.minusdi = bt.indicators.MinusDI(self.datas[0], period=self.params.adx_period)
# Volume indicator
self.volume_sma = bt.indicators.SMA(self.datavolume, period=self.params.volume_period)
# ATR for trailing stops
self.atr = bt.indicators.ATR(self.datas[0], period=self.params.atr_period)
# Track orders
self.order = None
self.trail_order = None
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
return
if order.status in [order.Completed]:
if order.isbuy():
self.log(f"BUY EXECUTED at {order.executed.price:.2f}")
elif order.issell():
self.log(f"SELL EXECUTED at {order.executed.price:.2f}")
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log(f"Order Canceled/Margin/Rejected: {order.getstatusname()}")
if order == self.order:
self.order = None
if order == self.trail_order:
self.trail_order = None
def notify_trade(self, trade):
if not trade.isclosed:
return
self.log(f"Trade Profit: GROSS {trade.pnl:.2f}, NET {trade.pnlcomm:.2f}")
def cancel_trail(self):
if self.trail_order:
self.cancel(self.trail_order)
self.trail_order = None
def next(self):
# Skip if order is pending
if self.order:
return
# Handle trailing stops for existing positions
if self.position:
if not self.trail_order:
if self.position.size > 0:
self.log(f"Placing ATR trailing stop for long position")
self.trail_order = self.sell(
exectype=bt.Order.StopTrail,
trailamount=self.atr[0] * self.params.atr_multiplier)
elif self.position.size < 0:
self.log(f"Placing ATR trailing stop for short position")
self.trail_order = self.buy(
exectype=bt.Order.StopTrail,
trailamount=self.atr[0] * self.params.atr_multiplier)
return
# Ensure sufficient data
if len(self) < 50: # Need enough bars for indicators
return
# Check ADX trend strength (simplified)
if self.adx[0] < self.params.adx_threshold:
return
# MACD crossover signals
macd_bullish = (self.macd.macd[0] > self.macd.signal[0] and
self.macd.macd[-1] <= self.macd.signal[-1])
macd_bearish = (self.macd.macd[0] < self.macd.signal[0] and
self.macd.macd[-1] >= self.macd.signal[-1])
# ADX directional confirmation
adx_bullish = self.plusdi[0] > self.minusdi[0]
adx_bearish = self.minusdi[0] > self.plusdi[0]
# Volume filter (simplified)
volume_ok = self.datavolume[0] > self.volume_sma[0]
# Entry conditions
long_signal = macd_bullish and adx_bullish and volume_ok
short_signal = macd_bearish and adx_bearish and volume_ok
if long_signal:
self.log(f"LONG signal at {self.dataclose[0]:.2f}")
self.cancel_trail()
if self.position and self.position.size < 0:
self.order = self.buy() # Close short and go long
elif not self.position:
self.order = self.buy()
elif short_signal:
self.log(f"SHORT signal at {self.dataclose[0]:.2f}")
self.cancel_trail()
if self.position and self.position.size > 0:
self.order = self.sell() # Close long and go short
elif not self.position:
self.order = self.sell()
def stop(self):
self.log(f"Ending Portfolio Value: {self.broker.getvalue():.2f}", doprint=True)
# Keep this tutorial backtest long-only.
original_sell = bt.Strategy.sell
def long_only_sell(self, *args, **kwargs):
if self.position.size <= 0:
return None
size = kwargs.get("size")
if size is None or size > self.position.size:
kwargs["size"] = self.position.size
return original_sell(self, *args, **kwargs)
bt.Strategy.sell = long_only_sell
# Download the exact hourly test window.
symbol = "XRP-USD"
data = yf.download(
symbol,
start=datetime(2025, 8, 14, 15, tzinfo=timezone.utc),
end=datetime(2026, 8, 14, 16, tzinfo=timezone.utc),
interval="1h",
auto_adjust=False,
progress=False,
)
if isinstance(data.columns, pd.MultiIndex):
data.columns = data.columns.get_level_values(0)
data = data[["Open", "High", "Low", "Close", "Volume"]].dropna()
if getattr(data.index, "tz", None) is not None:
data.index = data.index.tz_localize(None)
# Configure and run Backtrader.
cerebro = bt.Cerebro()
cerebro.broker.setcash(10_000)
cerebro.broker.setcommission(commission=0.001)
cerebro.addsizer(bt.sizers.PercentSizer, percents=95)
cerebro.adddata(bt.feeds.PandasData(dataname=data))
cerebro.addstrategy(MACDADXConfluenceStrategy)
cerebro.addanalyzer(bt.analyzers.DrawDown, _name="drawdown")
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name="trades")
result = cerebro.run()[0]
final_value = cerebro.broker.getvalue()
total_return = (final_value / 10_000 - 1) * 100
buy_hold_prices = data.loc[data.index.hour == 0, "Close"]
buy_hold_return = (buy_hold_prices.iloc[-1] / buy_hold_prices.iloc[0] - 1) * 100
excess_return = total_return - buy_hold_return
drawdown = result.analyzers.drawdown.get_analysis()
trades = result.analyzers.trades.get_analysis()
print(f"Final value: ${final_value:,.2f}")
print(f"Strategy return: {total_return:.2f}%")
print(f"Buy-and-hold return: {buy_hold_return:.2f}%")
print(f"Excess return: {excess_return:.2f} percentage points")
print(f"Maximum drawdown: {drawdown['max']['drawdown']:.2f}%")
print(f"Closed trades: {trades.get('total', {}).get('closed', 0)}")If you have the Mega Backtrader Strategy Pack, this command runs the full reporting pipeline for XRP-USD and automatically creates the CSV metrics and charts used below:
python run_backtest.py --symbol XRP-USD --period 1y --interval 1h --benchmark XRP-USD --strategy-filter MACDADXConfluenceStrategy.py --out hourly_crypto_results--allow-short.MACDADXConfluenceStrategy comes from the Mega
Backtrader Strategy Pack. The package includes the complete
MACDADXConfluenceStrategy.py source file and the backtest
runner used in this tutorial, so you can run the strategy, change its
parameters, and generate the same result files yourself.
This material is for research and education only. Backtests are hypothetical, sensitive to data and assumptions, and do not guarantee future performance.