Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Analyzing Market Returns, Volatility, Drawdowns, and Correlations

In our last session, we built a crucial tool for your analysis workflow: a Python function to retrieve and align daily price data. With a clean, time-synced dataset of prices for assets like Freeport-McMoRan (FCX), copper futures (HG=F), and the S&P 500, we are now ready to move from raw data to meaningful insights.

This lesson will guide you through calculating four fundamental metrics of market behavior: returns, volatility, drawdown, and correlation. These are not just abstract statistical concepts; they are the core quantitative measures traders use to assess performance, quantify risk, and understand the relationships between different assets in a portfolio. Mastering these calculations in Python will provide you with a powerful lens through which to view market dynamics.

Let's start with the function we created previously and the aligned data it produces. All the code in this lesson will assume you have this final_prices DataFrame ready.

# From the previous lesson
import yfinance as yf
import pandas as pd
import numpy as np # We will use NumPy extensively in this lesson

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 our assets and get the data
target_tickers = ["FCX", "HG=F", "^GSPC", "GBPUSD=X"]
start_period = "2021-01-01"
end_period = "2023-12-31"
final_prices = get_aligned_prices(target_tickers, start_period, end_period)

# Now, we'll work with the 'final_prices' DataFrame

1. Calculating Returns

Price levels are informative, but for analysis, we are almost always interested in the change in price, or the return. Returns standardize price movements and are the primary input for risk and performance analysis. There are two common ways to calculate them.

Simple (Arithmetic) Returns

This is the intuitive measure of return: the percentage change from one period to the next.

Where is the price at time . Pandas makes this incredibly simple with the .pct_change() method.

# Calculate daily simple returns
simple_returns = final_prices.pct_change()

# The first row will be NaN since there's no previous day to compare with
simple_returns = simple_returns.dropna() 

print("--- Simple Daily Returns (Head) ---")
print(simple_returns.head())

Log Returns

For many statistical models, log returns are preferred. They are defined as:

The key advantage of log returns is that they are additive over time. The total log return over multiple periods is simply the sum of the single-period log returns. This property is extremely convenient for modeling. Simple returns, by contrast, must be geometrically compounded.

To get a deeper understanding of the distinction and the mathematical convenience of log returns, the following reading is highly recommended.

NumPy Financial Examples

This section clearly explains the 'why' behind using log returns in quantitative finance, contrasting them with the more intuitive simple returns.

Focus on the explanations of why quants use log returns and the motivating example that demonstrates their additivity.

In Python, we can calculate log returns easily using NumPy's log function.

# Calculate daily log returns
log_returns = np.log(final_prices / final_prices.shift(1))

# Drop the initial NaN row
log_returns = log_returns.dropna()

print("\n--- Log Daily Returns (Head) ---")
print(log_returns.head())

For small return values, simple and log returns are nearly identical. For your analyses, using simple returns (pct_change) is perfectly fine and often more interpretable, so we will proceed with them for the rest of this lesson.

2. Measuring Volatility

Volatility is the magnitude of price fluctuation and is the most common measure of risk in finance. It's formally defined as the standard deviation of returns. Higher volatility means higher risk and greater uncertainty.

We can calculate the daily volatility using the .std() method on our returns DataFrame.

# Calculate daily volatility (standard deviation of simple returns)
daily_volatility = simple_returns.std()
print("--- Daily Volatility ---")
print(daily_volatility)

While daily volatility is useful, it's standard practice to quote volatility on an annualized basis to make comparisons easier. To annualize daily volatility, we multiply it by the square root of the number of trading days in a year (typically assumed to be 252).

This scaling works because variance grows linearly with time, and standard deviation (volatility) is the square root of variance.

# Annualize the volatility
annualized_volatility = daily_volatility * np.sqrt(252)
print("\n--- Annualized Volatility ---")
print(annualized_volatility)

This tells you that, for example, FCX has an annualized price volatility of around 43%, while the S&P 500 (^GSPC) is much lower, around 18%.

Rolling Volatility

Volatility is not constant; there are periods of calm and periods of turmoil. A rolling volatility calculation captures this dynamic by computing the standard deviation over a sliding window of time (e.g., the last 30 days). This is invaluable for understanding changing market regimes.

Quant Finance with Python and Pandas | 50 Concepts you NEED to Know in 9 Minutes | [Getting Started]

This video provides a quick, practical demonstration of calculating standard deviation and annualizing it.

Watch the segments on calculating volatility and annualizing volatility to see these concepts in action.

Here's how to calculate and plot the 30-day rolling volatility for FCX.

# Calculate 30-day rolling volatility for FCX
rolling_vol_fcx = simple_returns['FCX'].rolling(window=30).std() * np.sqrt(252)

# Plot the result
import matplotlib.pyplot as plt
plt.figure(figsize=(12, 6))
rolling_vol_fcx.plot()
plt.title('FCX: 30-Day Rolling Annualized Volatility')
plt.ylabel('Annualized Volatility')
plt.xlabel('Date')
plt.grid(True)
plt.show()

The resulting plot clearly shows how the risk profile of FCX changed over the period, with spikes in volatility corresponding to turbulent market events.

3. Calculating Drawdown

While volatility measures the general "wiggliness" of returns, drawdown measures a different, very intuitive aspect of risk: the percentage loss from an investment's highest peak to a subsequent low. The Maximum Drawdown (MDD) is the largest such drop an asset experienced over a period. It answers the question: "What's the most I could have lost if I bought at the absolute peak and sold at the absolute trough?"

Calculating drawdown involves a few steps:

  1. Compute a "wealth index" representing the cumulative growth of $1 invested at the start.
  2. Keep track of the "running maximum" of this wealth index.
  3. The drawdown at any point is the percentage difference between the current wealth index and the running maximum.

The following resource provides a complete walkthrough of this calculation, both conceptually and with NumPy code.

NumPy Financial Examples

This section gives a clear, step-by-step guide to calculating and visualizing drawdowns.

Follow the logic from the Wealth Index definition through to the NumPy implementation. The visualization code is also excellent for building intuition.

Let's apply this to our data for a single asset, FCX.

# 1. Calculate the Wealth Index for FCX
# We need to add 1 to the returns and then compute the cumulative product.
wealth_index_fcx = (1 + simple_returns['FCX']).cumprod()

# 2. Calculate the Previous Peaks
previous_peaks_fcx = wealth_index_fcx.cummax()

# 3. Calculate the Drawdown series
drawdown_fcx = (wealth_index_fcx - previous_peaks_fcx) / previous_peaks_fcx

# 4. Find the Maximum Drawdown
max_drawdown_fcx = drawdown_fcx.min()

print(f"Maximum Drawdown for FCX: {max_drawdown_fcx:.2%}")

# Plotting the wealth index and drawdown
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), sharex=True)

ax1.plot(wealth_index_fcx)
ax1.set_title('FCX: Wealth Index (Growth of $1)')
ax1.grid(True)

ax2.plot(drawdown_fcx, color='red')
ax2.fill_between(drawdown_fcx.index, drawdown_fcx, 0, color='red', alpha=0.3)
ax2.set_title('FCX: Drawdown')
ax2.yaxis.set_major_formatter(plt.FuncFormatter('{:.0%}'.format))
ax2.grid(True)

plt.show()

The charts powerfully illustrate periods of growth followed by painful drawdowns, providing a much richer picture of risk than a single volatility number.

4. Measuring Correlation

Correlation measures the degree to which two assets move in relation to each other. It is a value between -1 and +1:

  • +1: Perfect positive correlation. The assets move in lockstep.
  • 0: No correlation. The assets' movements are independent.
  • -1: Perfect negative correlation. The assets move in opposite directions.

For your goal, understanding the correlation between a commodity (copper) and a producer's stock (FCX) is fundamental. A high positive correlation suggests the stock is a good proxy for the commodity.

We can compute a correlation matrix for all assets in our returns DataFrame using the .corr() method.

# Calculate the correlation matrix of simple returns
correlation_matrix = simple_returns.corr()

print("--- Correlation Matrix ---")
print(correlation_matrix)

The matrix shows a correlation of about 0.58 between FCX and copper futures (HG=F). This is a moderately strong positive relationship, as expected. It also shows a strong positive correlation between FCX and the S&P 500 (^GSPC), indicating that FCX is sensitive to both copper prices and broader market sentiment.

To explore this relationship more deeply, we can examine the rolling correlation.

Python for Data Analysis, 3E - 11 Time Series

This is the definitive guide by the creator of pandas. This section shows the exact method for calculating rolling correlation between two series.

Focus on the example where the author computes the rolling correlation between Apple's returns and the S&P 500 returns. This is precisely the technique we will use.

Let's apply this to calculate the 60-day rolling correlation between FCX and copper.

# Calculate the 60-day rolling correlation between FCX and Copper (HG=F)
rolling_corr_fcx_hg = simple_returns['FCX'].rolling(window=60).corr(simple_returns['HG=F'])

# Plot the result
plt.figure(figsize=(12, 6))
rolling_corr_fcx_hg.plot()
plt.title('FCX vs. Copper (HG=F): 60-Day Rolling Correlation')
plt.ylabel('Correlation Coefficient')
plt.xlabel('Date')
plt.axhline(0, color='black', linestyle='--', linewidth=1)
plt.grid(True)
plt.show()

This plot reveals that the relationship is not static. There are periods where the correlation strengthens (the stock tracks the commodity closely) and periods where it weakens, providing crucial context for your trading decisions.

Conclusion

In this lesson, we have transformed our clean price data into a suite of powerful analytical metrics. You have moved beyond simply looking at price charts to quantifying an asset's behavior in a rigorous, reproducible way.

Here are your key takeaways:

  • Returns: We distinguished between simple and log returns and learned how to calculate them in pandas using .pct_change() and np.log().
  • Volatility: You can now calculate and annualize volatility (.std() * np.sqrt(252)) as a primary measure of risk, and also compute rolling volatility to see how risk evolves.
  • Drawdown: You learned to calculate the peak-to-trough loss of an asset, a critical measure of "pain" an investor would have endured, using a wealth index and cumulative maximums.
  • Correlation: You can now compute both a static correlation matrix (.corr()) and a dynamic rolling correlation to understand how relationships between assets change over time.

You are now equipped with the fundamental calculations used in quantitative finance. In our next lesson, we will build directly on these concepts to measure an equity's relative performance and rolling sensitivity to its underlying commodity. We will introduce the concept of "beta" and use the rolling correlation and volatility metrics you've just mastered to analyze the relationship between a stock and its commodity driver in even greater depth.

Can't find a good explanation? Sign up and we'll make it for you

Sign up