Welcome to the next lesson in our exploration of systematic market analysis. In the previous session, we focused on relative analysis, measuring how an equity like Freeport-McMoRan (FCX) performs and behaves in relation to its underlying commodity, copper. You learned to calculate relative performance and rolling sensitivity, giving you a quantitative handle on the relationship between two assets.
Today, we shift our focus from relative to absolute analysis. We will tackle a fundamental question for any single price series: is it trending, or is it moving sideways in a range? Answering this question is critical because trending markets reward trend-following strategies, while range-bound markets are better suited for mean-reversion approaches. Your ability to distinguish between these regimes using objective, repeatable methods is a cornerstone of systematic trading.
This lesson will equip you with two powerful, code-based techniques for classifying market behavior. We'll start with a classic method using moving averages and then move to a more sophisticated tool, the Average Directional Index (ADX), designed specifically for this task. By the end of our session, you'll be able to classify any price series as "trending" or "range-bound" based on explicit, reproducible rules.
Let's begin by setting up our Python environment, carrying over the data from our last lesson.
# Setup from the previous lesson
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# We will need the 'ta' library for the ADX indicator.
# If you don't have it installed, run: pip install ta
from ta.trend import ADXIndicator
def get_aligned_prices(tickers, start_date, end_date, interval="1d"):
# We need Open, High, Low, Close data for indicator calculations
data = yf.download(tickers, start=start_date, end=end_date, interval=interval)
if data.empty:
return None
# Use ffill to handle non-trading days, then drop any remaining NaNs at the start
data = data.fillna(method='ffill').dropna()
return data
# Define asset and get the data for FCX.
# We need the full OHLC data, not just Adj Close for today's lesson.
start_period = "2021-01-01"
end_period = "2023-12-31"
fcx_data = get_aligned_prices(["FCX"], start_period, end_period)
# Let's check the first few rows
print(fcx_data.head())
1. A Simple Method: The Long-Term Moving Average
One of the most straightforward ways to identify a trend is to use a long-term moving average (MA). An MA smooths out price action by calculating the average price over a specific number of periods. A 200-day MA, for instance, reflects the average closing price over the last 200 trading days, representing a long-term sentiment baseline.
The following video provides a clear, concise introduction to the moving average and how it's used to define market structure.
Moving Average: How To Quickly Identify A Trend Or Range Market (Video 3 Of 12)
This video by Rayner Teo explains the concept of moving averages and presents a simple ruleset for identifying trends.
Please watch from the beginning to understand how an MA is calculated. Then, pay close attention to the section where he explains how to use the 200-period MA to define market structure, from the rules.
As the video explains, we can define a simple, explicit set of rules:
- Uptrend: The price is consistently trading above the 200-day MA.
- Downtrend: The price is consistently trading below the 200-day MA.
- Range-bound: The price is "chopping" back and forth across the 200-day MA, which itself tends to be flat.
Let's implement this in Python and visualize it for FCX.
# Calculate the 200-day Simple Moving Average (SMA) for FCX
ma_period = 200
fcx_data['SMA_200'] = fcx_data['Adj Close'].rolling(window=ma_period).mean()
# Plot the price and the 200-day SMA
plt.figure(figsize=(15, 8))
plt.plot(fcx_data['Adj Close'], label='FCX Adj Close', color='blue', alpha=0.8)
plt.plot(fcx_data['SMA_200'], label=f'{ma_period}-Day SMA', color='red', linestyle='--')
plt.title('FCX Price vs. 200-Day Moving Average')
plt.ylabel('Price (USD)')
plt.xlabel('Date')
plt.legend()
plt.grid(True)
plt.show()
Looking at the chart, you can clearly see periods that align with our rules. For example, much of 2021 shows the price above the rising SMA, indicating an uptrend. In contrast, the period from mid-2022 to late-2022 shows the price crossing the flattening SMA multiple times, suggesting a range-bound market.
While simple and effective for identifying the long-term trend direction, this method can be ambiguous. What exactly qualifies as "chopping"? How do we quantify the strength of the trend? This leads us to a more specialized indicator.
2. A Robust Method: The Average Directional Index (ADX)
The Average Directional Index (ADX) was developed by J. Welles Wilder specifically to quantify trend strength, irrespective of its direction. It is a more robust tool for our classification task.
The indicator consists of three lines:
- +DI (Positive Directional Indicator): Measures the strength of the upward price movement.
- -DI (Negative Directional Indicator): Measures the strength of the downward price movement.
- ADX (Average Directional Index): A smoothed average of the difference between +DI and -DI. This line tells you how strong the trend is, whether it's up or down.
The following video gives an excellent conceptual overview of these three components and how they work together.
ADX and DMI Indicator for Trading: Best Strategies and Finding Trends
This video from Quantifica provides a clear visual explanation of the ADX and Directional Movement Index (DMI) system.
Focus on the initial segments that explain the three lines, the components of ADX, and how a simple threshold is used to identify trending markets.
The ADX Classification Rule
The ADX indicator is plotted on a scale from 0 to 100. Its value provides our explicit, reproducible rule for classifying the market state. The commonly accepted thresholds are detailed in the following resource.
Average Directional Index (ADX) | ChartSchool | StockCharts.com
This article from ChartSchool provides the definitive rules for interpreting ADX values.
Read the section Measuring Trend Strength. This will give you the specific numerical thresholds we will use to build our classification logic.
Based on these resources, our rule set is:
- ADX > 25: A strong trend is present.
- ADX < 20: The market is range-bound or non-trending.
- ADX between 20 and 25: This is an ambiguous "gray zone."
For our purposes, we will classify any period where ADX is greater than 25 as "Trending".
Understanding the Calculation
Your engineering background will appreciate that the ADX is not a black box. It's an algorithm built on a series of logical steps. A deep understanding isn't required to use it, but knowing the components helps in its interpretation. The following resource breaks down the math.
Mathematical Intuition of the ADX Indicator: A Python Approach
This QuantInsti article provides a step-by-step walkthrough of the calculations behind the ADX.
Read the section titled Calculation of ADX Indicator. Focus on understanding the sequence of operations: calculating True Range, then the positive and negative Directional Movements (+DM and -DM), and finally how these are smoothed and combined to produce the DI and ADX values.
3. Python Implementation and Visualization
Now, let's put this into practice. Instead of coding the entire ADX formula from scratch, we can use a well-established technical analysis library, ta, which is a common practice in development. This ensures accuracy and saves time.
The following code will calculate the ADX for FCX and then use our rule (ADX > 25) to classify each day as "Trending" or "Range-bound".
# Calculate ADX using the 'ta' library
# The standard period for ADX is 14 days
adx_indicator = ADXIndicator(
high=fcx_data['High'],
low=fcx_data['Low'],
close=fcx_data['Close'],
window=14,
fillna=True # Fill NaN values
)
fcx_data['ADX'] = adx_indicator.adx()
# Apply our classification rule
adx_threshold = 25
fcx_data['Trend_State'] = np.where(fcx_data['ADX'] > adx_threshold, 'Trending', 'Range-bound')
# Let's see the classification for the last few days
print(fcx_data[['Adj Close', 'ADX', 'Trend_State']].tail())
# --- Visualization ---
# Create a figure with two subplots
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(15, 10), sharex=True, gridspec_kw={'height_ratios': [3, 1]})
# Plot 1: Price
ax1.plot(fcx_data['Adj Close'], label='FCX Adj Close', color='blue')
ax1.set_title('FCX Price and Trend State')
ax1.set_ylabel('Price (USD)')
ax1.grid(True)
# Highlight trending periods on the price chart
# We create a new series that is NaN where not trending, and the price where it is.
trending_prices = np.where(fcx_data['Trend_State'] == 'Trending', fcx_data['Adj Close'], np.nan)
ax1.plot(fcx_data.index, trending_prices, color='orange', linewidth=3, label='Trending (ADX > 25)')
ax1.legend()
# Plot 2: ADX Indicator
ax2.plot(fcx_data['ADX'], label='ADX (14)', color='purple')
ax2.axhline(adx_threshold, color='red', linestyle='--', label=f'Threshold = {adx_threshold}')
ax2.set_title('Average Directional Index (ADX)')
ax2.set_ylabel('ADX Value')
ax2.set_xlabel('Date')
ax2.legend()
ax2.grid(True)
ax2.set_ylim(0, 75) # Set y-axis limit for better visibility of the relevant range
plt.tight_layout()
plt.show()
The resulting chart provides a powerful and clear visualization. The top panel shows the FCX share price, with the periods classified as "Trending" highlighted in orange. The bottom panel shows the ADX value that drives this classification. You can now see, with algorithmic precision, when the market was in a strong trend (either up or down) and when it was consolidating sideways.
Conclusion
In this lesson, you have learned to move beyond subjective chart reading and apply explicit, reproducible rules to classify market behavior. This is a fundamental skill for building systematic trading strategies.
Here are the key takeaways:
- Market Regimes Matter: Different strategies are required for trending versus range-bound markets. Identifying the current regime is the first step.
- Moving Average Method: A simple and intuitive way to gauge the long-term trend direction (price above/below a 200-day MA).
- ADX for Trend Strength: The Average Directional Index (ADX) provides a more robust, quantitative measure of trend strength.
- Explicit Rules: We established a clear rule—ADX > 25 indicates a trend—that can be programmed and applied consistently across any price series.
You have now built a Python workflow that ingests market data and automatically classifies it into distinct market states.
In our next lesson, we will take this a significant step further. Now that we can identify what the market is doing, we will begin to define rules for when to act. You will learn to implement entry and exit rules for a trading signal in Pine Script, the native scripting language of the popular TradingView platform, preparing you to backtest your ideas directly on charts.
Can't find a good explanation? Sign up and we'll make it for you
Sign up