Notebooks/ADX Trend Strength Strategy
Signals·TA Strategies·Beginner

ADX Trend Strength Strategy

Use ADX to measure trend strength and gate trades — only enter directional trades when ADX exceeds a configurable threshold.

ADXtrend strengthfilter

Strategy — ADX Trend Strength


1. Dependency Installation

[1]
!pip install pandas numpy plotly
Requirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (2.2.2)
Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (2.0.2)
Requirement already satisfied: plotly in /usr/local/lib/python3.12/dist-packages (5.24.1)
Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2)
Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas) (2026.1)
Requirement already satisfied: tenacity>=6.2.0 in /usr/local/lib/python3.12/dist-packages (from plotly) (9.1.4)
Requirement already satisfied: packaging in /usr/local/lib/python3.12/dist-packages (from plotly) (26.1)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)

2. Library Imports

[2]
import warnings
warnings.filterwarnings("ignore")
import pandas as pd
import numpy as np
import plotly.graph_objects as go

3. Strategy Overview

The Average Directional Index (ADX) strategy measures trend strength and direction simultaneously using three computed lines:

LineDefinition
ADXMeasures how strong the current trend is, regardless of direction. Range: 0–100.
+DIPositive Directional Indicator — measures upward price pressure.
−DINegative Directional Indicator — measures downward price pressure.

Signal logic:

  • ADX > threshold (e.g., 25) confirms a strong trend is present.
  • When +DI > −DI and ADX > threshold → Long signal (+1): upward trend confirmed.
  • When −DI > +DI and ADX > threshold → Short signal (−1): downward trend confirmed.
  • When ADX < threshold → No signal (0): market is ranging; trend-following is unreliable.

ADX does not predict direction — it only confirms whether a trend is strong enough to trade. The directional lines (+DI, −DI) determine which direction that trend is moving. This combination prevents entering trend-following trades in sideways, choppy markets where such strategies typically lose money.


4. Data Generation

[3]
def generate_data(periods: int) -> pd.DataFrame:
    start_date     = pd.to_datetime("2024-01-01 00:00:00+00:00")
    datetime_index = pd.date_range(start_date, periods=periods, freq="1min", tz="UTC")

    price_data = []; last_close = 42000
    volatility_scale = 0.005; wick_deviation_scale = 0.002

    for i in range(periods):
        open_price   = last_close + np.random.normal(0, last_close * volatility_scale * 0.1)
        price_change = np.random.normal(0, last_close * volatility_scale)
        close_price  = open_price + price_change

        body_high    = max(open_price, close_price)
        body_low     = min(open_price, close_price)

        high_price   = body_high + abs(np.random.normal(0, last_close * wick_deviation_scale))
        low_price    = body_low  - abs(np.random.normal(0, last_close * wick_deviation_scale))

        high_price   = max(high_price, open_price, close_price)
        low_price    = min(low_price,  open_price, close_price)

        if high_price < low_price:
            high_price, low_price = low_price, high_price

        price_data.append({
            "open":  max(1, int(open_price)),
            "high":  max(1, int(high_price)),
            "low":   max(1, int(low_price)),
            "close": max(1, int(close_price)),
        })
        last_close = close_price

    df = pd.DataFrame(price_data, index=datetime_index)
    df.index.name = "datetime"
    df["volume"] = np.random.uniform(100.0, 500.0, periods)
    df["datetime"] = df.index.to_series()
    return df.reset_index(drop=True)

df = generate_data(500)

print("--- Dataset Shape ---")
display(df.head())
df.info()
--- Dataset Shape ---
open high low close volume datetime
0 41988 42297 41984 42213 306.723688 2024-01-01 00:00:00+00:00
1 42172 42214 42139 42165 353.233686 2024-01-01 00:01:00+00:00
2 42185 42193 41634 41652 292.996563 2024-01-01 00:02:00+00:00
3 41632 41633 41342 41489 443.654398 2024-01-01 00:03:00+00:00
4 41485 41521 41268 41321 247.855748 2024-01-01 00:04:00+00:00
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 500 entries, 0 to 499
Data columns (total 6 columns):
 #   Column    Non-Null Count  Dtype              
---  ------    --------------  -----              
 0   open      500 non-null    int64              
 1   high      500 non-null    int64              
 2   low       500 non-null    int64              
 3   close     500 non-null    int64              
 4   volume    500 non-null    float64            
 5   datetime  500 non-null    datetime64[ns, UTC]
dtypes: datetime64[ns, UTC](1), float64(1), int64(4)
memory usage: 23.6 KB

Explanation: Five hundred 1-minute candles are generated with realistic open-high-low-close relationships, including proper wick extensions beyond the body. This provides sufficient history for ADX (which requires at least 2× the lookback period to stabilize) to produce reliable signals.


5. ADX Strategy Function

[4]
def adx_trend_strength_strategy(
    df:            pd.DataFrame,
    window:        int   = 14,
    adx_threshold: float = 25.0,
) -> pd.DataFrame:
    df = df.copy().sort_values("datetime", ignore_index=True)

    # --- True Range ---
    hl  = df["high"] - df["low"]
    hpc = (df["high"] - df["close"].shift(1)).abs()
    lpc = (df["low"]  - df["close"].shift(1)).abs()
    tr  = pd.concat([hl, hpc, lpc], axis=1).max(axis=1)

    # --- Directional Movement ---
    up_move   = df["high"] - df["high"].shift(1)
    down_move = df["low"].shift(1) - df["low"]

    plus_dm  = np.where((up_move > down_move) & (up_move > 0),   up_move,   0.0)
    minus_dm = np.where((down_move > up_move) & (down_move > 0), down_move, 0.0)

    atr_roll = tr.rolling(window).sum()
    plus_di  = 100 * pd.Series(plus_dm).rolling(window).sum()  / atr_roll
    minus_di = 100 * pd.Series(minus_dm).rolling(window).sum() / atr_roll
    dx       = 100 * (plus_di - minus_di).abs() / (plus_di + minus_di).replace(0, np.nan)
    adx      = dx.rolling(window).mean()

    df["+DI"]   = plus_di.values
    df["-DI"]   = minus_di.values
    df["adx"]   = adx.values

    # --- Signal ---
    df["signal"] = np.where(
        (df["adx"] > adx_threshold) & (df["+DI"] > df["-DI"]),   1,
        np.where(
        (df["adx"] > adx_threshold) & (df["-DI"] > df["+DI"]), -1, 0)
    )
    return df

df_signals = adx_trend_strength_strategy(df, window=14, adx_threshold=25.0)

Explanation:

  • True Range (TR): Captures the full price movement including gaps between sessions by taking the maximum of three distance measurements — current high to low, high to previous close, and low to previous close.
  • +DM / −DM: Directional movement isolates whether upward or downward price excursions are dominant in each period.
  • Smoothed ATR and DI: Rolling sums over the window period smooth noise out of both the range and directional components.
  • DX and ADX: The DX computes the relative strength of direction as a percentage; ADX smooths DX to produce a stable trend-strength reading. Values above 25 reliably distinguish trending from ranging conditions.
  • Signal gate: ADX acts as a gate — only when trend strength is confirmed does the +DI/−DI comparison determine direction.

6. Signal Summary

[7]
print("--- Signal Distribution ---")
print(df_signals["signal"].value_counts())

print("\n--- ADX Statistics ---")
print(df_signals["adx"].describe().round(2))

display(df_signals[["datetime","close","+DI","-DI","adx","signal"]].dropna().head(20))
--- Signal Distribution ---
signal
 1    190
 0    163
-1    147
Name: count, dtype: int64

--- ADX Statistics ---
count    474.00
mean      35.35
std       16.04
min        8.33
25%       22.76
50%       32.86
75%       42.05
max       93.34
Name: adx, dtype: float64
datetime close +DI -DI adx signal
26 2024-01-01 00:26:00+00:00 41567 18.780971 26.313181 24.292940 0
27 2024-01-01 00:27:00+00:00 41512 20.005278 20.137239 22.744651 0
28 2024-01-01 00:28:00+00:00 41759 23.170129 18.743818 21.913936 0
29 2024-01-01 00:29:00+00:00 41428 18.751530 20.440636 21.293932 0
30 2024-01-01 00:30:00+00:00 41403 16.091395 20.296548 20.920003 0
31 2024-01-01 00:31:00+00:00 41409 13.722209 21.635190 19.543098 0
32 2024-01-01 00:32:00+00:00 40969 10.107817 28.301887 19.213603 0
33 2024-01-01 00:33:00+00:00 40949 8.277765 30.673718 19.436656 0
34 2024-01-01 00:34:00+00:00 41130 12.875641 20.620572 20.218830 0
35 2024-01-01 00:35:00+00:00 40849 12.610672 21.703757 21.242253 0
36 2024-01-01 00:36:00+00:00 40882 12.723322 21.390633 22.264914 0
37 2024-01-01 00:37:00+00:00 40896 13.151984 19.990017 23.074530 0
38 2024-01-01 00:38:00+00:00 40877 15.224192 16.657977 21.182179 0
39 2024-01-01 00:39:00+00:00 40905 14.013453 17.909193 20.218972 0
40 2024-01-01 00:40:00+00:00 40532 10.772521 27.746592 22.173495 0
41 2024-01-01 00:41:00+00:00 40406 10.718085 31.409574 25.658312 -1
42 2024-01-01 00:42:00+00:00 40741 12.793121 30.771235 27.851703 -1
43 2024-01-01 00:43:00+00:00 40881 16.160764 31.028668 29.794348 -1
44 2024-01-01 00:44:00+00:00 40898 19.040698 32.093023 30.792164 -1
45 2024-01-01 00:45:00+00:00 41433 29.114249 26.546855 29.523061 1

Explanation: The signal distribution reveals how often the market is trending strongly enough to trade. A large proportion of 0 signals indicates a predominantly ranging dataset — expected for random walk synthetic data. ADX statistics confirm the average trend strength.


7. Visualization

[6]
buy_signals  = df_signals[df_signals["signal"] ==  1]
sell_signals = df_signals[df_signals["signal"] == -1]

fig = go.FigureWidget(data=[go.Candlestick(
    x=df_signals["datetime"],
    open=df_signals["open"], high=df_signals["high"],
    low=df_signals["low"],   close=df_signals["close"],
    name="Price"
)])

fig.add_trace(go.Scatter(
    x=buy_signals["datetime"],
    y=buy_signals["low"] * 0.999,
    mode="markers",
    marker=dict(symbol="triangle-up",   size=10, color="green"),
    name="Buy Signal (+1)"
))

fig.add_trace(go.Scatter(
    x=sell_signals["datetime"],
    y=sell_signals["high"] * 1.001,
    mode="markers",
    marker=dict(symbol="triangle-down", size=10, color="red"),
    name="Sell Signal (−1)"
))

fig.add_trace(go.Scatter(
    x=df_signals["datetime"], y=df_signals["adx"],
    mode="lines", name="ADX",
    line=dict(color="purple", width=1), yaxis="y2"
))

fig.add_trace(go.Scatter(
    x=df_signals["datetime"], y=df_signals["+DI"],
    mode="lines", name="+DI",
    line=dict(color="green", width=1, dash="dot"), yaxis="y2"
))

fig.add_trace(go.Scatter(
    x=df_signals["datetime"], y=df_signals["-DI"],
    mode="lines", name="−DI",
    line=dict(color="red", width=1, dash="dot"), yaxis="y2"
))

fig.update_layout(
    title_text="ADX Trend Strength Strategy — Signals",
    xaxis_rangeslider_visible=False,
    xaxis_title="Datetime", yaxis_title="Price",
    yaxis2=dict(title="ADX / DI", overlaying="y", side="right", range=[0, 60]),
    height=600,
    yaxis=dict(autorange=True),
)
fig.show()

Explanation: Buy signals (green triangles below bars) appear when ADX confirms trend strength and +DI dominates. Sell signals (red triangles above bars) appear when ADX confirms strength and −DI dominates. The secondary axis displays ADX and both DI lines, allowing visual confirmation that signals align with the ADX threshold crossings.

[ ]