Recommended resources
The freely available resources this lesson will direct you to.
Welcome to the next step in our Python-driven market analysis workflow. In our last session, you mastered the calculation of essential risk and performance metrics: returns, volatility, drawdown, and correlation. You saw, for instance, that Freeport-McMoRan (FCX) has a moderately strong but dynamic rolling correlation with copper prices (HG=F).
This lesson builds directly on that foundation. We will now move from observing correlation to precisely measuring an equity's performance relative to its underlying commodity and quantifying its sensitivity to commodity price movements. You will learn to answer critical questions for a commodity-linked equity trader:
- Is the stock outperforming or underperforming the commodity right now?
- Exactly how much does the stock price tend to move for every 1% move in the commodity price?
- Does this sensitivity change over time, and what does that tell us about the stock's behavior?
By the end of this lesson, you will be able to construct and interpret plots of relative performance and rolling sensitivity, adding another layer of sophisticated, quantitative evidence to your trade analysis.
Let's begin by setting up our environment with the code and data from the previous lesson.
# Setup from the previous lesson
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import statsmodels.api as sm # We'll use this for regression analysis
def get_aligned_prices(tickers, start_date, end_date, interval="1d"):
data = yf.download(tickers, start=start_date, end=end_date, interval=interval)
if data.empty:
return None
adj_close = data['Adj Close']
if isinstance(adj_close, pd.Series):
adj_close = adj_close.to_frame(name=tickers[0])
aligned_prices = adj_close.fillna(method='ffill').dropna()
return aligned_prices
# Define assets and get the data
target_tickers = ["FCX", "HG=F", "^GSPC"] # Let's focus on FCX, Copper, and the S&P 500
start_period = "2021-01-01"
end_period = "2023-12-31"
final_prices = get_aligned_prices(target_tickers, start_period, end_period)
# Calculate simple daily returns
simple_returns = final_prices.pct_change().dropna()
1. Measuring Relative Performance
A simple correlation coefficient tells us about the direction of the relationship, but not its magnitude over time. To see which asset has performed better, we can visualize their cumulative returns.
Cumulative Returns (Wealth Index)
As we saw in the previous lesson on drawdown, we can calculate a "wealth index" that shows the growth of $1 invested in each asset. Plotting these for both the equity and the commodity on the same chart provides a clear visual comparison of performance.
# Calculate the wealth index for each asset
wealth_index = (1 + simple_returns).cumprod()
# Plot the wealth indices
plt.figure(figsize=(14, 7))
wealth_index['FCX'].plot(label='FCX (Equity)', color='blue')
wealth_index['HG=F'].plot(label='Copper Futures (Commodity)', color='green')
wealth_index['^GSPC'].plot(label='S&P 500 (Market)', color='gray', linestyle='--')
plt.title('Growth of $1: FCX vs. Copper vs. S&P 500')
plt.ylabel('Cumulative Return')
plt.xlabel('Date')
plt.legend()
plt.grid(True)
plt.show()
This plot immediately reveals periods where FCX outperformed copper, and vice-versa. You can also see how both are performing relative to the broader market. This is a high-level performance dashboard.
Price Ratio Analysis
Another powerful and direct way to measure relative performance is to calculate the price ratio of the equity to the commodity.
- An increasing ratio indicates the equity is outperforming the commodity.
- A decreasing ratio indicates the equity is underperforming the commodity.
Traders often analyze the trend of this ratio to generate signals. Let's calculate and plot it for FCX and copper.
# Calculate the price ratio
price_ratio = final_prices['FCX'] / final_prices['HG=F']
# Plot the price ratio with a rolling average to smooth it out
plt.figure(figsize=(14, 7))
price_ratio.plot(label='FCX/Copper Price Ratio', alpha=0.5)
price_ratio.rolling(window=30).mean().plot(label='30-Day Moving Average', color='red')
plt.title('Relative Performance: FCX vs. Copper')
plt.ylabel('Price Ratio (FCX / HG=F)')
plt.xlabel('Date')
plt.legend()
plt.grid(True)
plt.show()
The trend in this chart is your direct measure of relative performance. A sustained uptrend means FCX is a better investment than holding copper directly over that period, and a downtrend means the opposite.
2. Quantifying Sensitivity with Beta (β)
Visual analysis is insightful, but to quantify sensitivity, we turn to linear regression. In finance, the sensitivity of an asset's returns to a benchmark's returns is called beta (β). It's the slope of the best-fit line through a scatter plot of their returns.
The model we are fitting is:
- β (beta): The slope. If beta is 1.5, we expect the equity to move 1.5% for every 1% move in the commodity. This is a measure of leveraged exposure.
- α (alpha): The intercept. It represents the equity's return that is not explained by the commodity's movement. A positive alpha might suggest outperformance due to company-specific factors.
The following video provides an excellent primer on calculating beta using Python's statsmodels library. While it uses the S&P 500 as the market benchmark, the concept and code are directly applicable to our case of using a commodity as the benchmark.
How to get the Stock Beta with Python - CAPM Regression
Watch this video from Algovibes to understand the concept of beta and how to calculate it using regression.
Pay close attention to the theoretical explanation of beta and its interpretation. Then, focus on the Python implementation using the statsmodels library. We will adapt this exact approach.
Now, let's apply this to find the static beta of FCX relative to copper over our entire sample period.
# Define our dependent (Y) and independent (X) variables
Y = simple_returns['FCX']
X = simple_returns['HG=F']
# Add a constant to the independent variable (for the alpha intercept)
X = sm.add_constant(X)
# Create and fit the Ordinary Least Squares (OLS) model
model = sm.OLS(Y, X).fit()
# Print the model summary
print(model.summary())
In the results table, look for the coef column:
- The coefficient for
constis the alpha (α). - The coefficient for
HG=Fis the beta (β).
The R-squared value tells you what percentage of FCX's return variance is "explained" by copper's return variance. This is simply the square of the correlation coefficient we calculated in the last lesson. This beta value gives you a single number for the stock's sensitivity over the entire period.
3. Measuring Rolling Sensitivity
A single beta value is an average over a long period and can mask significant changes in the relationship. A commodity producer's stock might track its underlying commodity very closely during stable times but decouple during a market panic or due to company-specific news (e.g., a mine strike, a major acquisition).
To capture this, we calculate the rolling beta. This involves running the same regression but on a sliding window of data (e.g., the last 90 days). Your experience with complex data manipulation will be useful here, as this requires applying a custom function over a rolling window in pandas.
# Define a function to calculate beta from a window of data
# It will be applied to each rolling window
def calculate_rolling_beta(df, independent_col, dependent_col):
"""
Calculates the beta coefficient from a pandas DataFrame of returns.
`df` is the data from a single rolling window.
"""
# Isolate the columns
X = df[independent_col]
Y = df[dependent_col]
# Add a constant (for the intercept alpha)
X_const = sm.add_constant(X, has_constant='add')
# Fit the regression model
try:
# Some windows might have insufficient data or no variance,
# so we use a try-except block for robustness.
model = sm.OLS(Y, X_const, missing='drop').fit()
# Return the beta coefficient for the independent variable
return model.params[independent_col]
except (ValueError, np.linalg.LinAlgError):
# Return NaN if regression fails for the window
return np.nan
# Define the window size for the rolling calculation
window_size = 90 # 90 trading days is approx. 4 months
# Select the two return series we need
returns_subset = simple_returns[['FCX', 'HG=F']].dropna()
# Calculate the rolling beta
# .apply() passes a DataFrame of the window to our function
# raw=False is crucial for passing a DataFrame, not a NumPy array
rolling_beta = returns_subset.rolling(window=window_size).apply(
lambda df: calculate_rolling_beta(df, independent_col='HG=F', dependent_col='FCX'),
raw=False
).dropna()
# Plot the rolling beta
plt.figure(figsize=(14, 7))
rolling_beta.plot()
plt.title(f'FCX: {window_size}-Day Rolling Beta to Copper (HG=F)')
plt.ylabel('Beta (β)')
plt.xlabel('Date')
plt.axhline(rolling_beta.mean(), color='red', linestyle='--', label=f'Average Beta: {rolling_beta.mean():.2f}')
plt.axhline(1.0, color='black', linestyle='--', label='Beta = 1.0')
plt.legend()
plt.grid(True)
plt.show()
This final plot is a powerful analytical tool. It shows you how FCX's leverage to the copper price has evolved.
- When beta is above 1.0, the stock is acting as a leveraged play on copper.
- When beta is below 1.0, it is exhibiting lower sensitivity than the commodity itself.
- A trending beta (either up or down) can be as significant as a trend in price, indicating a fundamental shift in how the market is pricing the stock relative to its primary driver.
Conclusion
In this lesson, you've added two powerful techniques to your analytical toolkit, moving from basic correlation to a more nuanced understanding of inter-asset relationships.
Your key takeaways are:
- Relative Performance: You can measure this visually by comparing cumulative returns (wealth indices) or by analysing the trend of the price ratio between an equity and its commodity.
- Sensitivity (Beta): You learned that beta (β) from a linear regression quantifies the sensitivity of an equity's returns to a commodity's returns.
- Rolling Beta: You implemented a rolling regression in Python to see how this sensitivity changes over time, providing crucial context that a single, static beta number cannot.
These techniques for measuring relative performance and rolling sensitivity are central to constructing a thesis on a commodity-linked equity. You can now quantitatively assess whether a stock is a good proxy for a commodity, whether it offers leverage, and how that relationship behaves through different market regimes.
In our next lesson, we will shift our focus from relative analysis to absolute analysis. You will learn how to classify a price series as trending or range-bound using explicit and reproducible chart rules. This will form the basis for developing systematic trading signals.