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