Lesson 14 · Real-World Data Analytics
Python Data Analytics #14: Volatility Analysis & Rolling Risk Metrics in Python
Video fourteen of the hundred-video real-world data analytics series. Measuring real risk itself as it changes over time, using rolling windows on real…
- CourseReal-World Data Analytics
- Lesson14 of 26
- Video32 min
- FormatJupyter notebook · 30 code cells
- Data1 dataset
What you'll learn
Datasets used in this lesson
Save these next to the notebook. In Google Colab, upload them with the 📁 icon on the left first.
- aapl_tsla_merged.csv17.9 KB
📓 Full notebook
Download .ipynbData Analytics 100, Video 14: Volatility Analysis and Rolling Risk Metrics#
- Video fourteen of the hundred-video real-world data analytics series.
- Measuring real risk itself as it changes over time, using rolling windows on real Apple and real Tesla returns.
- Let's get into it.
Part 1: Risk Is Not Constant#
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
merged = pd.read_csv('aapl_tsla_merged.csv', parse_dates=['Date'])
merged.shape[0]
Part 2: Real 20-Day Rolling Volatility#
merged['AAPL_Vol20'] = merged['AAPL_Return'].rolling(20).std() * np.sqrt(252)
merged['TSLA_Vol20'] = merged['TSLA_Return'].rolling(20).std() * np.sqrt(252)
merged[['Date', 'AAPL_Vol20', 'TSLA_Vol20']].dropna().head(3).round(3)
Part 3: Real 60-Day Rolling Volatility#
merged['AAPL_Vol60'] = merged['AAPL_Return'].rolling(60).std() * np.sqrt(252)
merged['TSLA_Vol60'] = merged['TSLA_Return'].rolling(60).std() * np.sqrt(252)
merged[['AAPL_Vol20', 'AAPL_Vol60']].dropna().corr().round(3)
Part 4: Real Rolling Volatility Summary Stats#
aapl_vol_summary = merged['AAPL_Vol20'].dropna().describe().round(3)
tsla_vol_summary = merged['TSLA_Vol20'].dropna().describe().round(3)
aapl_vol_summary
tsla_vol_summary
Part 5: Visualizing Real Rolling Volatility#
plt.figure(figsize=(11, 6))
plt.plot(merged['Date'], merged['AAPL_Vol20'], label='Real Apple 20-Day Vol')
plt.plot(merged['Date'], merged['TSLA_Vol20'], label='Real Tesla 20-Day Vol')
plt.xlabel('Real Date')
plt.ylabel('Real Annualized Volatility')
plt.title('Real Rolling 20-Day Volatility, Apple vs Tesla')
plt.legend()
plt.tight_layout()
plt.savefig('rolling_volatility.png', dpi=120)
plt.close()
Part 6: Real Volatility Clustering Check#
vol_today = merged['AAPL_Vol20'].dropna()
vol_next_day = vol_today.shift(-1).dropna()
aligned_today = vol_today.loc[vol_next_day.index]
round(aligned_today.corr(vol_next_day), 3)
Part 7: Real Rolling Sharpe Ratio#
aapl_roll_return20 = merged['AAPL_Return'].rolling(20).mean() * 252
merged['AAPL_RollSharpe20'] = aapl_roll_return20 / merged['AAPL_Vol20']
tsla_roll_return20 = merged['TSLA_Return'].rolling(20).mean() * 252
merged['TSLA_RollSharpe20'] = tsla_roll_return20 / merged['TSLA_Vol20']
merged[['Date', 'AAPL_RollSharpe20', 'TSLA_RollSharpe20']].dropna().tail(3).round(2)
Part 8: Visualizing Real Rolling Sharpe Ratio#
plt.figure(figsize=(11, 5))
plt.plot(merged['Date'], merged['AAPL_RollSharpe20'], label='Real Apple Rolling Sharpe')
plt.plot(merged['Date'], merged['TSLA_RollSharpe20'], label='Real Tesla Rolling Sharpe')
plt.axhline(0, color='gray', linestyle='--')
plt.xlabel('Real Date')
plt.ylabel('Real Rolling Sharpe Ratio')
plt.title('Real Rolling 20-Day Sharpe Ratio, Apple vs Tesla')
plt.legend()
plt.tight_layout()
plt.savefig('rolling_sharpe.png', dpi=120)
plt.close()
Part 9: Real Downside Deviation#
def rolling_downside_vol(returns, window):
def only_losses(x):
losses = x[x < 0]
return losses.std() * np.sqrt(252) if len(losses) > 1 else np.nan
return returns.rolling(window).apply(only_losses, raw=False)
merged['AAPL_DownsideVol20'] = rolling_downside_vol(merged['AAPL_Return'], 20)
merged['AAPL_DownsideVol20'].dropna().describe().round(3)
Part 10: Real Downside vs Real Total Volatility#
downside_ratio = (merged['AAPL_DownsideVol20'] / merged['AAPL_Vol20']).dropna()
round(downside_ratio.mean(), 3)
Part 11: Real High-Volatility Regime Flagging#
aapl_median_vol = merged['AAPL_Vol20'].median()
merged['AAPL_HighVolFlag'] = merged['AAPL_Vol20'] > (aapl_median_vol * 1.5)
merged['AAPL_HighVolFlag'].sum()
Part 12: Real Returns During High-Volatility Regimes#
high_vol_returns = merged.loc[merged['AAPL_HighVolFlag'], 'AAPL_Return']
normal_returns = merged.loc[~merged['AAPL_HighVolFlag'].fillna(False), 'AAPL_Return']
round(high_vol_returns.mean() * 100, 3), round(normal_returns.mean() * 100, 3)
Part 13: Real Rolling Maximum Drawdown#
def rolling_max_drawdown(cumulative, window):
roll_peak = cumulative.rolling(window, min_periods=1).max()
drawdown = (cumulative - roll_peak) / roll_peak
return drawdown.rolling(window).min()
merged['AAPL_RollMaxDD60'] = rolling_max_drawdown(merged['AAPL_Cumulative'], 60)
merged['AAPL_RollMaxDD60'].dropna().describe().round(3)
Part 14: Visualizing Real Rolling Drawdown#
plt.figure(figsize=(11, 5))
plt.fill_between(merged['Date'], merged['AAPL_RollMaxDD60'] * 100, 0, color='crimson', alpha=0.5)
plt.xlabel('Real Date')
plt.ylabel('Real Rolling 60-Day Max Drawdown (%)')
plt.title('Real Rolling Maximum Drawdown, Apple')
plt.tight_layout()
plt.savefig('rolling_drawdown.png', dpi=120)
plt.close()
Part 15: Real Worst Rolling Drawdown Window#
worst_dd_idx = merged['AAPL_RollMaxDD60'].idxmin()
worst_dd_date = merged.loc[worst_dd_idx, 'Date']
worst_dd_value = round(merged.loc[worst_dd_idx, 'AAPL_RollMaxDD60'] * 100, 2)
worst_dd_date, worst_dd_value
Part 16: Real Volatility Ratio, Tesla vs Apple#
merged['VolRatio_TSLA_AAPL'] = merged['TSLA_Vol20'] / merged['AAPL_Vol20']
merged['VolRatio_TSLA_AAPL'].dropna().describe().round(2)
Part 17: Real Rolling Metrics Table Preview#
risk_cols = ['Date', 'AAPL_Vol20', 'AAPL_Vol60', 'TSLA_Vol20', 'TSLA_Vol60', 'AAPL_RollSharpe20', 'TSLA_RollSharpe20', 'AAPL_DownsideVol20', 'AAPL_RollMaxDD60', 'VolRatio_TSLA_AAPL']
merged[risk_cols].dropna().tail(3).round(3)
Part 18: Saving the Real Rolling Risk Table#
merged[risk_cols].round(4).to_csv('rolling_risk_metrics.csv', index=False)
reloaded = pd.read_csv('rolling_risk_metrics.csv')
reloaded.shape[0] == merged.shape[0]
Part 19: Real Sanity Check on Annualization#
manual_check = merged['AAPL_Return'].tail(20).std() * np.sqrt(252)
pipeline_value = merged['AAPL_Vol20'].iloc[-1]
round(abs(manual_check - pipeline_value), 8) < 1e-6
Part 20: Real Recap Print#
aapl_mean_vol_pct = round(merged['AAPL_Vol20'].mean() * 100, 1)
tsla_mean_vol_pct = round(merged['TSLA_Vol20'].mean() * 100, 1)
print(f'Across {merged.shape[0]} real overlapping trading days, Apple averaged {aapl_mean_vol_pct}% annualized rolling volatility versus Tesla at {tsla_mean_vol_pct}%.')
Part 21: Real EWMA Volatility (RiskMetrics Style)#
aapl_sq_returns = merged['AAPL_Return'] ** 2
ewma_variance = aapl_sq_returns.ewm(alpha=0.06, adjust=False).mean()
merged['AAPL_EWMA_Vol'] = np.sqrt(ewma_variance) * np.sqrt(252)
merged['AAPL_EWMA_Vol'].dropna().describe().round(3)
Part 22: Real EWMA vs Real Simple Rolling Volatility#
plt.figure(figsize=(11, 5))
plt.plot(merged['Date'], merged['AAPL_Vol20'], label='Real Simple 20-Day Vol')
plt.plot(merged['Date'], merged['AAPL_EWMA_Vol'], label='Real EWMA Vol')
plt.xlabel('Real Date')
plt.ylabel('Real Annualized Volatility')
plt.title('Real Simple Rolling vs Real EWMA Volatility, Apple')
plt.legend()
plt.tight_layout()
plt.savefig('ewma_vs_rolling_vol.png', dpi=120)
plt.close()
Part 23: Real Rolling Sortino Ratio#
merged['AAPL_RollSortino20'] = aapl_roll_return20 / merged['AAPL_DownsideVol20']
merged[['Date', 'AAPL_RollSharpe20', 'AAPL_RollSortino20']].dropna().tail(3).round(2)
Part 24: Real Tesla High-Volatility Regime#
tsla_median_vol = merged['TSLA_Vol20'].median()
merged['TSLA_HighVolFlag'] = merged['TSLA_Vol20'] > (tsla_median_vol * 1.5)
merged['AAPL_HighVolFlag'].sum(), merged['TSLA_HighVolFlag'].sum()
Part 25: Real Historical Value-at-Risk on Rolling Windows#
def rolling_var_95(returns, window):
return returns.rolling(window).apply(lambda x: np.percentile(x, 5), raw=True)
merged['AAPL_RollVaR95_60'] = rolling_var_95(merged['AAPL_Return'], 60)
round(merged['AAPL_RollVaR95_60'].dropna().mean() * 100, 2)
Part 26: Real Tightest and Real Widest VaR Windows#
tightest_var_date = merged.loc[merged['AAPL_RollVaR95_60'].idxmax(), 'Date']
widest_var_date = merged.loc[merged['AAPL_RollVaR95_60'].idxmin(), 'Date']
tightest_var_date, widest_var_date
Part 27: Real Correlation Between Volatility and Drawdown#
vol_dd_pair = merged[['AAPL_Vol60', 'AAPL_RollMaxDD60']].dropna()
round(vol_dd_pair['AAPL_Vol60'].corr(vol_dd_pair['AAPL_RollMaxDD60']), 3)
Part 28: Real Current Volatility Percentile Rank#
current_vol = merged['AAPL_Vol20'].dropna().iloc[-1]
vol_percentile = (merged['AAPL_Vol20'].dropna() < current_vol).mean() * 100
round(vol_percentile, 1)
Part 29: Saving the Real Extended Risk Table#
extended_cols = risk_cols + ['AAPL_EWMA_Vol', 'AAPL_RollSortino20', 'TSLA_HighVolFlag', 'AAPL_RollVaR95_60']
merged[extended_cols].round(4).to_csv('rolling_risk_metrics_extended.csv', index=False)
reloaded_ext = pd.read_csv('rolling_risk_metrics_extended.csv')
reloaded_ext.shape[1] == len(extended_cols)
Wrap-Up: What You Learned#
- Volatility genuinely changes over time, and rolling windows are how you make that real variation visible instead of averaging it away.
- Rolling Sharpe ratios show how a real risk-adjusted return itself shifts as market conditions change.
- Downside deviation isolates the real risk investors actually worry about, losses, rather than penalizing real upside swings too.
- Flagging real high-volatility regimes lets you test whether returns genuinely behave differently during turbulent stretches.
- Rolling maximum drawdown tracks the real worst pain an investor would have felt within any given real window, not just once at the end.
- Next video: real correlation and diversification across a wider set of real assets, going beyond just this one real pair.
Found this useful?
All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.



