← Back to Home
MACD ADX Confluence XRP-USD Hourly Tutorial 20.04% Return

MACD ADX Confluence XRP-USD Hourly Tutorial 20.04% Return

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.

The Result First

MACDADXConfluenceStrategy hourly backtest results

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.

What We Are Building

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.

Step 0: Install and Download Hourly Data

Install the minimal dependencies:

pip install backtrader yfinance pandas matplotlib numpy

Then 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()

Step 1: define the confluence parameters

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),
)

Step 2: initialize momentum and directional indicators

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)

Step 3: detect the bullish MACD crossover

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]
)

Step 4: confirm trend strength and volume

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]

Step 5: combine all entry conditions

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()

Step 6: place the two-ATR trailing stop

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,
)

Connect the Strategy to Backtrader

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}")

Complete Copy-and-Run Script

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)}")

Run the Packaged Backtest

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

Read the Charts

XRP-USD: strategy versus buy-and-hold

MACDADXConfluenceStrategy XRP-USD versus buy-and-hold

XRP-USD: drawdown versus buy-and-hold

MACDADXConfluenceStrategy XRP-USD drawdown versus buy-and-hold

XRP-USD: strategy return distribution

MACDADXConfluenceStrategy XRP-USD return distribution

How to Interpret This Result

A Better Research Workflow

  1. Save the downloaded dataset and record its checksum so every future run uses the same input bars.
  2. Reserve untouched validation months, then run walk-forward rather than selecting on the entire year.
  3. Test nearby parameters. A robust idea should not collapse when a period changes by one or two bars.
  4. Add spread, slippage, funding, and exchange-specific order rules—especially for high-turnover strategies.
  5. Inspect every order lifecycle and test the short-enabled branch separately with --allow-short.
  6. Compare the one-hour configuration with time-equivalent settings on four-hour and daily bars.

Continue with the Full Package

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.

Disclaimer

This material is for research and education only. Backtests are hypothetical, sensitive to data and assumptions, and do not guarantee future performance.