Signals·TA Strategy Implementations·Intermediate

ADX Trend Strength Strategy

Implement an ADX-based trend strength filtering strategy that uses the Average Directional Index to quantify trend strength, combining DI+ and DI- crossovers with minimum ADX thresholds for higher-quality entry signals.

trading-signalstrading-strategies

Strategy — ADX Trend Strength


1. Dependency Installation

[ ]
!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

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

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

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

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

[ ]
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.

Conclusion

This notebook demonstrates the implementation of an ADX-based trend strength strategy. The Average Directional Index (ADX) is a valuable tool for identifying the presence and strength of a trend, while the Positive Directional Indicator (+DI) and Negative Directional Indicator (-DI) determine the trend's direction.

The strategy logic involves:

  • Trend Confirmation: ADX values above a predefined threshold (e.g., 25) indicate a strong trend.
  • Directional Signals: When ADX confirms a trend, a long signal (+1) is generated if +DI is greater than -DI, indicating an upward trend. Conversely, a short signal (-1) is generated if -DI is greater than +DI, indicating a downward trend.
  • No-Trade Zones: When ADX is below the threshold, no signal (0) is generated, advising against trend-following trades in ranging or choppy markets.

By combining trend strength and direction, this strategy aims to reduce false signals and improve the reliability of entries in trending markets. The visualization further aids in understanding how signals are generated in relation to price action and the ADX/DI lines.