ADX Trend Strength Strategy
Use ADX to measure trend strength and gate trades — only enter directional trades when ADX exceeds a configurable threshold.
Strategy — ADX Trend Strength
1. Dependency Installation
!pip install pandas numpy plotlyRequirement 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 go3. Strategy Overview
The Average Directional Index (ADX) strategy measures trend strength and direction simultaneously using three computed lines:
| Line | Definition |
|---|---|
| ADX | Measures how strong the current trend is, regardless of direction. Range: 0–100. |
| +DI | Positive Directional Indicator — measures upward price pressure. |
| −DI | Negative 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
windowperiod 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.