Show code
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy import statsThree Crisis Episodes at the Bloomberg Terminal
This lab is designed for the Financial Innovation Lab with Bloomberg Terminal access.
Duration: 2 hours Prerequisites: Complete Lab 4 Homework (GARCH estimation on simulated and SPY data)
In the lecture we saw that GARCH models are not merely passive measurement tools. Under stress conditions, the risk management systems built on them can amplify the very volatility they attempt to measure. This is the endogenous risk argument: when all institutions use the same model, the model becomes the systemic risk (Danielsson 2002; Danielsson, Shin, and Zigrand 2012).
Today you will investigate whether that feedback mechanism is visible in real data across three distinct episodes. The question is not whether these crises happened : they clearly did : but whether the signature of the feedback loop is detectable, and whether the three crises share the same structural fingerprint.
For each part, you will use the BDH (Bloomberg Data History) function, save to CSV, and analyse in Python. A template BDH call looks like this:
=BDH("VIX Index", "PX_LAST", "20170101", "20171231", "Days", "A")
where "A" returns actual trading days only.
Before examining stress episodes, you need a benchmark for what the VIX-to-realised relationship looks like in calm conditions. The year 2017 is ideal: it was historically one of the least volatile years on record, providing a clean baseline against which the crisis episodes will stand in sharp relief.
In Excel:
=BDH("VIX Index", "PX_LAST", "20170101", "20171231", "Days", "A")
=BDH("SPX Index", "PX_LAST", "20170101", "20171231", "Days", "A")
Save as baseline_2017.csv with columns Date, VIX, SPX.
df_base = pd.read_csv('baseline_2017.csv', parse_dates=['Date'], index_col='Date')
df_base.columns = ['VIX', 'SPX']
df_base['Return'] = df_base['SPX'].pct_change() * 100
df_base['RV_21'] = df_base['Return'].rolling(21).std() * np.sqrt(252)
df_base['VRP'] = df_base['VIX'] - df_base['RV_21']
print("=== 2017 Baseline Statistics ===")
print(f"Average VIX: {df_base['VIX'].mean():.1f}")
print(f"Average Realised Vol: {df_base['RV_21'].mean():.1f}")
print(f"Average VRP: {df_base['VRP'].mean():.1f}")
print(f"Days Premium > 0: {(df_base['VRP'] > 0).mean()*100:.0f}%")
print(f"Max Daily VIX: {df_base['VIX'].max():.1f}")Record your baseline statistics in the deliverables table. Why is 2017 an unusually good calibration year? What market conditions produced such persistently low VIX readings, and why might this calm itself have created the conditions for what followed in early 2018?
The collapse of Lehman Brothers on 15 September 2008 triggered the most significant test of risk model architecture since the Great Depression. What makes this episode instructive is not simply that volatility was extreme : it is that the escalation had a discernible structure. Risk model breaches triggered deleveraging, which generated price falls, which triggered further breaches. The question is whether that sequence is visible in the data.
=BDH("VIX Index", "PX_LAST", "20080101", "20090401", "Days", "A")
=BDH("SPX Index", "PX_LAST", "20080101", "20090401", "Days", "A")
=BDH("XLF US Equity","PX_LAST", "20080101", "20090401", "Days", "A")
Save as crisis_2008.csv with columns Date, VIX, SPX, XLF.
XLF is the SPDR Financial Sector ETF : the epicentre of 2008. Comparing it against the broader SPX index lets you track how the crisis moved from its origin in the financial sector outward.
df_08 = pd.read_csv('crisis_2008.csv', parse_dates=['Date'], index_col='Date')
df_08.columns = ['VIX', 'SPX', 'XLF']
df_08['SPX_ret'] = df_08['SPX'].pct_change() * 100
df_08['XLF_ret'] = df_08['XLF'].pct_change() * 100
df_08['RV_21'] = df_08['SPX_ret'].rolling(21).std() * np.sqrt(252)
df_08['VRP'] = df_08['VIX'] - df_08['RV_21']
lehman = pd.Timestamp('2008-09-15')
spx_low = pd.Timestamp('2009-03-06')fig, axes = plt.subplots(3, 1, figsize=(12, 10), sharex=True)
axes[0].plot(df_08.index, df_08['VIX'], color='coral', linewidth=1.2)
axes[0].axvline(lehman, color='black', linestyle='--', linewidth=1, label='Lehman (15 Sep 2008)')
axes[0].axvline(spx_low, color='darkred', linestyle='--', linewidth=1, label='SPX low (6 Mar 2009)')
axes[0].set_ylabel('VIX')
axes[0].set_title('2008 GFC: Tracing the Feedback Spiral')
axes[0].legend(fontsize=8)
axes[1].plot(df_08.index, df_08['SPX_ret'], linewidth=0.5, color='steelblue', label='SPX')
axes[1].plot(df_08.index, df_08['XLF_ret'], linewidth=0.5, color='firebrick', alpha=0.7, label='XLF (Financials)')
axes[1].axhline(0, color='gray', linewidth=0.5)
axes[1].axvline(lehman, color='black', linestyle='--', linewidth=1)
axes[1].set_ylabel('Daily Return (%)')
axes[1].legend(fontsize=8)
axes[2].plot(df_08.index, df_08['VRP'], color='purple', linewidth=1)
axes[2].axhline(0, color='k', linestyle='--', linewidth=0.5)
axes[2].axvline(lehman, color='black', linestyle='--', linewidth=1)
axes[2].set_ylabel('VIX − Realised Vol (VRP)')
axes[2].set_xlabel('Date')
plt.tight_layout()
plt.show()pre = df_08[df_08.index < lehman].dropna(subset=['VRP'])
post = df_08[df_08.index >= lehman].dropna(subset=['VRP'])
print("=== Pre vs Post Lehman ===")
print(f"{'Metric':<28} {'Pre-Lehman':>12} {'Post-Lehman':>12}")
print("-" * 54)
print(f"{'Average VIX':<28} {pre['VIX'].mean():>12.1f} {post['VIX'].mean():>12.1f}")
print(f"{'Average Realised Vol':<28} {pre['RV_21'].mean():>12.1f} {post['RV_21'].mean():>12.1f}")
print(f"{'Average VRP':<28} {pre['VRP'].mean():>12.1f} {post['VRP'].mean():>12.1f}")
print(f"{'Days VRP < 0 (%)':<28} {(pre['VRP']<0).mean()*100:>11.0f}% {(post['VRP']<0).mean()*100:>11.0f}%")During the worst months of the crisis the VRP turned negative : implied volatility was lower than realised volatility. In normal times the premium is positive and compensates option sellers for bearing risk. What does a negative premium in a crisis mean? Is this evidence that options markets were underpricing risk, or is there another explanation involving the dynamics of forced buying and selling?
The events of 5 February 2018 are pedagogically important precisely because there was no fundamental economic trigger. The US economy was healthy, earnings were strong, and growth was accelerating. What destroyed the XIV exchange-traded note : which had accumulated billions of dollars by systematically selling volatility : was a rule baked into its own prospectus. When volatility spiked enough, the product was contractually required to buy VIX futures, which drove volatility higher, which triggered further buying. It was a death spiral engineered into the product’s design.
Examining the VIX futures term structure : the relationship between near-term and longer-dated futures prices : lets you distinguish this mechanical episode from a crisis driven by sustained fundamental deterioration.
=BDH("VIX Index", "PX_LAST", "20180101", "20180228", "Days", "A")
=BDH("SPX Index", "PX_LAST", "20180101", "20180228", "Days", "A")
=BDH("VX1 Comdty", "PX_LAST", "20180101", "20180228", "Days", "A")
=BDH("VX2 Comdty", "PX_LAST", "20180101", "20180228", "Days", "A")
Save as volmageddon_2018.csv with columns Date, VIX, SPX, VX1, VX2.
VX1 and VX2 are the front-month and second-month VIX futures contracts. In normal conditions, VX2 trades above VX1 (contango) : the market prices medium-term uncertainty higher than near-term. When a crisis is expected to persist, the structure inverts (backwardation). A spike with rapid reversion to contango is the signature of a mechanical, not a fundamental, event.
df_vm = pd.read_csv('volmageddon_2018.csv', parse_dates=['Date'], index_col='Date')
df_vm.columns = ['VIX', 'SPX', 'VX1', 'VX2']
df_vm['SPX_ret'] = df_vm['SPX'].pct_change() * 100
df_vm['RV_21'] = df_vm['SPX_ret'].rolling(21).std() * np.sqrt(252)
df_vm['VRP'] = df_vm['VIX'] - df_vm['RV_21']
df_vm['Term_Spread'] = df_vm['VX2'] - df_vm['VX1']
event = pd.Timestamp('2018-02-05')fig, axes = plt.subplots(3, 1, figsize=(12, 9), sharex=True)
axes[0].plot(df_vm.index, df_vm['VIX'], color='coral', linewidth=1.5)
axes[0].axvline(event, color='black', linestyle='--', linewidth=1.5, label='5 Feb 2018')
axes[0].set_ylabel('VIX')
axes[0].set_title('Volmageddon: A Mechanically-Driven Spike')
axes[0].legend()
axes[1].plot(df_vm.index, df_vm['VX1'], label='VX1 (Front Month)', color='steelblue', linewidth=1.2)
axes[1].plot(df_vm.index, df_vm['VX2'], label='VX2 (Second Month)', color='darkblue', linewidth=1.2, linestyle='--')
axes[1].axvline(event, color='black', linestyle='--', linewidth=1.5)
axes[1].set_ylabel('VIX Futures Price')
axes[1].legend()
axes[2].plot(df_vm.index, df_vm['Term_Spread'], color='purple', linewidth=1.2)
axes[2].axhline(0, color='k', linestyle='--', linewidth=0.8, label='Contango/Backwardation boundary')
axes[2].axvline(event, color='black', linestyle='--', linewidth=1.5)
axes[2].set_ylabel('VX2 − VX1')
axes[2].set_xlabel('Date')
axes[2].legend(fontsize=8)
plt.tight_layout()
plt.show()# Use the nearest available trading dates
pre_event = df_vm[df_vm.index < event].iloc[-1]
on_event = df_vm[df_vm.index >= event].iloc[0]
post_event = df_vm[df_vm.index > event].iloc[4] # ~1 week later
print("=== Volmageddon: Key Dates ===")
print(f"{'Metric':<22} {'Pre-event':>12} {'5 Feb':>12} {'1 Week Later':>14}")
print("-" * 62)
print(f"{'VIX':<22} {pre_event['VIX']:>12.1f} {on_event['VIX']:>12.1f} {post_event['VIX']:>14.1f}")
print(f"{'VX1 (Front Month)':<22} {pre_event['VX1']:>12.1f} {on_event['VX1']:>12.1f} {post_event['VX1']:>14.1f}")
print(f"{'Term Spread (VX2-VX1)':<22} {pre_event['Term_Spread']:>12.2f} {on_event['Term_Spread']:>12.2f} {post_event['Term_Spread']:>14.2f}")Examine how the term structure behaved on and after 5 February. Did the curve invert (move into backwardation) at the peak of the spike? How quickly did it recover to contango? Compare this to what you would expect if the market believed the volatility shock was permanent. What does the term structure tell you that the VIX level alone cannot?
The COVID crash is the cleanest available illustration of a liquidity-driven crisis. In a fundamental crisis, asset prices fall because the economic outlook genuinely deteriorates. In a liquidity crisis, assets that bear no fundamental relation to one another sell off simultaneously because institutional investors : facing margin calls or redemption requests : need to raise cash quickly, selling whatever is liquid. The two mechanisms produce different cross-asset correlation patterns, and Bloomberg data lets you see the difference directly.
=BDH("VIX Index", "PX_LAST", "20200101", "20200430", "Days", "A")
=BDH("SPX Index", "PX_LAST", "20200101", "20200430", "Days", "A")
=BDH("LQD US Equity", "PX_LAST", "20200101", "20200430", "Days", "A")
=BDH("HYG US Equity", "PX_LAST", "20200101", "20200430", "Days", "A")
=BDH("GLD US Equity", "PX_LAST", "20200101", "20200430", "Days", "A")
=BDH("TLT US Equity", "PX_LAST", "20200101", "20200430", "Days", "A")
Save as covid_2020.csv with columns Date, VIX, SPX, LQD, HYG, GLD, TLT.
LQD is the iShares investment-grade corporate bond ETF, HYG is the iShares high-yield corporate bond ETF, GLD tracks gold, and TLT tracks long-dated US Treasuries : each a canonical safe-haven or diversifier in normal markets.
df_cv = pd.read_csv('covid_2020.csv', parse_dates=['Date'], index_col='Date')
df_cv.columns = ['VIX', 'SPX', 'LQD', 'HYG', 'GLD', 'TLT']
assets = ['SPX', 'LQD', 'HYG', 'GLD', 'TLT']
for col in assets:
df_cv[f'{col}_ret'] = df_cv[col].pct_change() * 100
df_cv['SPX_ret_raw'] = df_cv['SPX'].pct_change() * 100
df_cv['RV_21'] = df_cv['SPX_ret_raw'].rolling(21).std() * np.sqrt(252)
df_cv['VRP'] = df_cv['VIX'] - df_cv['RV_21']
ret_cols = [f'{c}_ret' for c in assets]
crash_start = pd.Timestamp('2020-02-19')
crash_trough = pd.Timestamp('2020-03-23')calm = df_cv[df_cv.index < crash_start][ret_cols].dropna()
crisis = df_cv[(df_cv.index >= crash_start) & (df_cv.index <= crash_trough)][ret_cols].dropna()
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
im1 = axes[0].imshow(calm.corr(), cmap='RdYlGn', vmin=-1, vmax=1)
axes[0].set_xticks(range(len(assets)))
axes[0].set_yticks(range(len(assets)))
axes[0].set_xticklabels(assets, rotation=45)
axes[0].set_yticklabels(assets)
axes[0].set_title('Correlations: Calm Period (Jan–18 Feb 2020)')
plt.colorbar(im1, ax=axes[0])
im2 = axes[1].imshow(crisis.corr(), cmap='RdYlGn', vmin=-1, vmax=1)
axes[1].set_xticks(range(len(assets)))
axes[1].set_yticks(range(len(assets)))
axes[1].set_xticklabels(assets, rotation=45)
axes[1].set_yticklabels(assets)
axes[1].set_title('Correlations: Crisis Period (19 Feb–23 Mar 2020)')
plt.colorbar(im2, ax=axes[1])
plt.suptitle('COVID-19: Cross-Asset Correlation Breakdown', fontsize=12, y=1.02)
plt.tight_layout()
plt.show()print("=== SPX Pairwise Correlations: Calm vs Crisis ===")
print(f"{'Asset':<8} {'Calm':>10} {'Crisis':>10} {'Change':>10}")
print("-" * 40)
for a in ['LQD', 'HYG', 'GLD', 'TLT']:
c_calm = calm['SPX_ret'].corr(calm[f'{a}_ret'])
c_crisis = crisis['SPX_ret'].corr(crisis[f'{a}_ret'])
print(f"{a:<8} {c_calm:>10.3f} {c_crisis:>10.3f} {c_crisis - c_calm:>+10.3f}")fig, axes = plt.subplots(2, 1, figsize=(12, 8), sharex=True)
axes[0].plot(df_cv.index, df_cv['VIX'], color='coral', linewidth=1.5, label='VIX')
axes[0].axvspan(crash_start, crash_trough, alpha=0.15, color='red', label='Crisis window')
axes[0].legend()
axes[0].set_ylabel('VIX')
axes[0].set_title('COVID-19: VIX and Cross-Asset Returns')
for col, colour in zip(['SPX', 'GLD', 'TLT'], ['steelblue', 'gold', 'green']):
scaled = df_cv[col] / df_cv[col].iloc[0] * 100
axes[1].plot(df_cv.index, scaled, label=col, linewidth=1, color=colour)
axes[1].axvspan(crash_start, crash_trough, alpha=0.15, color='red')
axes[1].set_ylabel('Indexed Price (1 Jan 2020 = 100)')
axes[1].set_xlabel('Date')
axes[1].legend()
plt.tight_layout()
plt.show()Gold and long-dated Treasuries are canonical safe-haven assets : they are expected to appreciate when equities fall. Examine your correlation table and timeline chart. On approximately what date did this safe-haven relationship break down? What does its breakdown tell you about the nature of the selling pressure at that point? When : and why : did safe-haven behaviour resume?
Having examined three distinct episodes with real Bloomberg data, you are now in a position to answer the central question the lab set out at the start: do these crises share a structural fingerprint, and is the feedback mechanism proposed in the lecture visible in each of them?
# Ensure VRP is computed for all dataframes
for df in [df_base, df_08, df_vm, df_cv]:
if 'VRP' not in df.columns:
df['VRP'] = df['VIX'] - df['RV_21']
episodes = {
'2017 Baseline': df_base,
'2008 GFC': df_08,
'2018 Volmageddon': df_vm,
'2020 COVID': df_cv,
}
print("=== Cross-Episode Comparison ===")
print(f"{'Episode':<22} {'Mean VIX':>10} {'Peak VIX':>10} {'Mean VRP':>10} {'Days VRP<0':>12}")
print("-" * 66)
for name, df in episodes.items():
vrp_clean = df['VRP'].dropna()
print(
f"{name:<22} "
f"{df['VIX'].mean():>10.1f} "
f"{df['VIX'].max():>10.1f} "
f"{vrp_clean.mean():>10.1f} "
f"{(vrp_clean < 0).mean()*100:>11.0f}%"
)Address the following questions in 250–350 words and submit with your deliverables.
Speed of onset. Rank the three crisis episodes by how quickly VIX escalated from its pre-event level to its peak. What does the speed of escalation tell you about the underlying mechanism in each case?
The VRP sign reversal. In which episodes did the volatility risk premium turn negative : that is, implied volatility fell below realised volatility? What does this mean for an investor who had been systematically harvesting the premium by selling volatility? Is this reversal a coincidence, or an intrinsic feature of the feedback mechanism?
Fundamental versus mechanical. Based on your data, which of the three episodes was most clearly driven by the endogenous feedback mechanism described in the lecture, rather than by deteriorating economic fundamentals? Justify your answer with specific evidence from your charts and statistics.
Implications for GARCH-based risk management. If you were a risk manager using GARCH-estimated VaR to set position limits, in which episode would your model have given you the least warning before the worst days? Why does this matter for the design of regulatory capital frameworks?
Submit the following before the next session:
Data summary table
| Metric | 2017 Baseline | 2008 GFC | 2018 Volmageddon | 2020 COVID |
|---|---|---|---|---|
| Mean VIX | ||||
| Peak VIX | ||||
| Mean VRP | ||||
| Days VRP < 0 (%) |
Charts : export and label clearly:
Written synthesis (250–350 words, see above)
Connection to homework. Compare the GARCH-estimated conditional volatility from your SPY model (Lab 4 Homework) to the VIX series extracted today. In the 2020 episode, which measure responded more quickly? What does this difference reveal about the relative nature of implied versus historical volatility as risk signals?
---
title: "Lab 4 In-Class: Forensic Volatility Analysis"
subtitle: "Three Crisis Episodes at the Bloomberg Terminal"
format:
html:
toc: true
toc-depth: 2
number-sections: true
execute:
echo: true
warning: false
message: false
eval: false
bibliography:
- ../resources/reading.bib
---
::: {.callout-important}
### In-Class Lab : Bloomberg Terminal Required
This lab is designed for the **Financial Innovation Lab** with Bloomberg Terminal access.
**Duration**: 2 hours
**Prerequisites**: Complete Lab 4 Homework (GARCH estimation on simulated and SPY data)
:::
## The Central Question
In the lecture we saw that GARCH models are not merely passive measurement tools. Under stress conditions, the risk management systems built on them can amplify the very volatility they attempt to measure. This is the endogenous risk argument: when all institutions use the same model, the model becomes the systemic risk [@danielsson2002emperor; @danielsson2012endogenous].
Today you will investigate whether that feedback mechanism is *visible in real data* across three distinct episodes. The question is not whether these crises happened : they clearly did : but whether the signature of the feedback loop is detectable, and whether the three crises share the same structural fingerprint.
## Bloomberg Setup
1. Log in to the Bloomberg Terminal
2. Open Excel, then close it
3. Install the **Bloomberg Excel Add-in** from the home screen (required each session)
4. Re-open Excel and begin data extraction
For each part, you will use the **BDH** (Bloomberg Data History) function, save to CSV, and analyse in Python. A template BDH call looks like this:
```
=BDH("VIX Index", "PX_LAST", "20170101", "20171231", "Days", "A")
```
where `"A"` returns actual trading days only.
## Part 1: Calibration : What Does Normal Look Like?
Before examining stress episodes, you need a benchmark for what the VIX-to-realised relationship looks like in calm conditions. The year 2017 is ideal: it was historically one of the least volatile years on record, providing a clean baseline against which the crisis episodes will stand in sharp relief.
### Bloomberg Extraction
In Excel:
```
=BDH("VIX Index", "PX_LAST", "20170101", "20171231", "Days", "A")
=BDH("SPX Index", "PX_LAST", "20170101", "20171231", "Days", "A")
```
Save as `baseline_2017.csv` with columns `Date`, `VIX`, `SPX`.
### Python Analysis
```{python}
#| label: setup
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
```
```{python}
#| label: baseline-analysis
df_base = pd.read_csv('baseline_2017.csv', parse_dates=['Date'], index_col='Date')
df_base.columns = ['VIX', 'SPX']
df_base['Return'] = df_base['SPX'].pct_change() * 100
df_base['RV_21'] = df_base['Return'].rolling(21).std() * np.sqrt(252)
df_base['VRP'] = df_base['VIX'] - df_base['RV_21']
print("=== 2017 Baseline Statistics ===")
print(f"Average VIX: {df_base['VIX'].mean():.1f}")
print(f"Average Realised Vol: {df_base['RV_21'].mean():.1f}")
print(f"Average VRP: {df_base['VRP'].mean():.1f}")
print(f"Days Premium > 0: {(df_base['VRP'] > 0).mean()*100:.0f}%")
print(f"Max Daily VIX: {df_base['VIX'].max():.1f}")
```
::: {.callout-tip}
### Discussion 1.1
Record your baseline statistics in the deliverables table. Why is 2017 an unusually good calibration year? What market conditions produced such persistently low VIX readings, and why might this calm itself have created the conditions for what followed in early 2018?
:::
## Part 2: The 2008 Global Financial Crisis
The collapse of Lehman Brothers on 15 September 2008 triggered the most significant test of risk model architecture since the Great Depression. What makes this episode instructive is not simply that volatility was extreme : it is that the escalation had a discernible structure. Risk model breaches triggered deleveraging, which generated price falls, which triggered further breaches. The question is whether that sequence is visible in the data.
### Bloomberg Extraction
```
=BDH("VIX Index", "PX_LAST", "20080101", "20090401", "Days", "A")
=BDH("SPX Index", "PX_LAST", "20080101", "20090401", "Days", "A")
=BDH("XLF US Equity","PX_LAST", "20080101", "20090401", "Days", "A")
```
Save as `crisis_2008.csv` with columns `Date`, `VIX`, `SPX`, `XLF`.
XLF is the SPDR Financial Sector ETF : the epicentre of 2008. Comparing it against the broader SPX index lets you track how the crisis moved from its origin in the financial sector outward.
### Python Analysis
```{python}
#| label: gfc-load
df_08 = pd.read_csv('crisis_2008.csv', parse_dates=['Date'], index_col='Date')
df_08.columns = ['VIX', 'SPX', 'XLF']
df_08['SPX_ret'] = df_08['SPX'].pct_change() * 100
df_08['XLF_ret'] = df_08['XLF'].pct_change() * 100
df_08['RV_21'] = df_08['SPX_ret'].rolling(21).std() * np.sqrt(252)
df_08['VRP'] = df_08['VIX'] - df_08['RV_21']
lehman = pd.Timestamp('2008-09-15')
spx_low = pd.Timestamp('2009-03-06')
```
```{python}
#| label: gfc-chart
fig, axes = plt.subplots(3, 1, figsize=(12, 10), sharex=True)
axes[0].plot(df_08.index, df_08['VIX'], color='coral', linewidth=1.2)
axes[0].axvline(lehman, color='black', linestyle='--', linewidth=1, label='Lehman (15 Sep 2008)')
axes[0].axvline(spx_low, color='darkred', linestyle='--', linewidth=1, label='SPX low (6 Mar 2009)')
axes[0].set_ylabel('VIX')
axes[0].set_title('2008 GFC: Tracing the Feedback Spiral')
axes[0].legend(fontsize=8)
axes[1].plot(df_08.index, df_08['SPX_ret'], linewidth=0.5, color='steelblue', label='SPX')
axes[1].plot(df_08.index, df_08['XLF_ret'], linewidth=0.5, color='firebrick', alpha=0.7, label='XLF (Financials)')
axes[1].axhline(0, color='gray', linewidth=0.5)
axes[1].axvline(lehman, color='black', linestyle='--', linewidth=1)
axes[1].set_ylabel('Daily Return (%)')
axes[1].legend(fontsize=8)
axes[2].plot(df_08.index, df_08['VRP'], color='purple', linewidth=1)
axes[2].axhline(0, color='k', linestyle='--', linewidth=0.5)
axes[2].axvline(lehman, color='black', linestyle='--', linewidth=1)
axes[2].set_ylabel('VIX − Realised Vol (VRP)')
axes[2].set_xlabel('Date')
plt.tight_layout()
plt.show()
```
```{python}
#| label: gfc-prepost
pre = df_08[df_08.index < lehman].dropna(subset=['VRP'])
post = df_08[df_08.index >= lehman].dropna(subset=['VRP'])
print("=== Pre vs Post Lehman ===")
print(f"{'Metric':<28} {'Pre-Lehman':>12} {'Post-Lehman':>12}")
print("-" * 54)
print(f"{'Average VIX':<28} {pre['VIX'].mean():>12.1f} {post['VIX'].mean():>12.1f}")
print(f"{'Average Realised Vol':<28} {pre['RV_21'].mean():>12.1f} {post['RV_21'].mean():>12.1f}")
print(f"{'Average VRP':<28} {pre['VRP'].mean():>12.1f} {post['VRP'].mean():>12.1f}")
print(f"{'Days VRP < 0 (%)':<28} {(pre['VRP']<0).mean()*100:>11.0f}% {(post['VRP']<0).mean()*100:>11.0f}%")
```
::: {.callout-tip}
### Discussion 2.1
During the worst months of the crisis the VRP turned *negative* : implied volatility was lower than realised volatility. In normal times the premium is positive and compensates option sellers for bearing risk. What does a negative premium in a crisis mean? Is this evidence that options markets were underpricing risk, or is there another explanation involving the dynamics of forced buying and selling?
:::
## Part 3: Volmageddon : February 2018
The events of 5 February 2018 are pedagogically important precisely because there was no fundamental economic trigger. The US economy was healthy, earnings were strong, and growth was accelerating. What destroyed the XIV exchange-traded note : which had accumulated billions of dollars by systematically selling volatility : was a rule baked into its own prospectus. When volatility spiked enough, the product was contractually required to buy VIX futures, which drove volatility higher, which triggered further buying. It was a death spiral engineered into the product's design.
Examining the VIX futures term structure : the relationship between near-term and longer-dated futures prices : lets you distinguish this mechanical episode from a crisis driven by sustained fundamental deterioration.
### Bloomberg Extraction
```
=BDH("VIX Index", "PX_LAST", "20180101", "20180228", "Days", "A")
=BDH("SPX Index", "PX_LAST", "20180101", "20180228", "Days", "A")
=BDH("VX1 Comdty", "PX_LAST", "20180101", "20180228", "Days", "A")
=BDH("VX2 Comdty", "PX_LAST", "20180101", "20180228", "Days", "A")
```
Save as `volmageddon_2018.csv` with columns `Date`, `VIX`, `SPX`, `VX1`, `VX2`.
VX1 and VX2 are the front-month and second-month VIX futures contracts. In normal conditions, VX2 trades above VX1 (contango) : the market prices medium-term uncertainty higher than near-term. When a crisis is expected to persist, the structure inverts (backwardation). A spike with rapid reversion to contango is the signature of a mechanical, not a fundamental, event.
### Python Analysis
```{python}
#| label: vm-load
df_vm = pd.read_csv('volmageddon_2018.csv', parse_dates=['Date'], index_col='Date')
df_vm.columns = ['VIX', 'SPX', 'VX1', 'VX2']
df_vm['SPX_ret'] = df_vm['SPX'].pct_change() * 100
df_vm['RV_21'] = df_vm['SPX_ret'].rolling(21).std() * np.sqrt(252)
df_vm['VRP'] = df_vm['VIX'] - df_vm['RV_21']
df_vm['Term_Spread'] = df_vm['VX2'] - df_vm['VX1']
event = pd.Timestamp('2018-02-05')
```
```{python}
#| label: vm-chart
fig, axes = plt.subplots(3, 1, figsize=(12, 9), sharex=True)
axes[0].plot(df_vm.index, df_vm['VIX'], color='coral', linewidth=1.5)
axes[0].axvline(event, color='black', linestyle='--', linewidth=1.5, label='5 Feb 2018')
axes[0].set_ylabel('VIX')
axes[0].set_title('Volmageddon: A Mechanically-Driven Spike')
axes[0].legend()
axes[1].plot(df_vm.index, df_vm['VX1'], label='VX1 (Front Month)', color='steelblue', linewidth=1.2)
axes[1].plot(df_vm.index, df_vm['VX2'], label='VX2 (Second Month)', color='darkblue', linewidth=1.2, linestyle='--')
axes[1].axvline(event, color='black', linestyle='--', linewidth=1.5)
axes[1].set_ylabel('VIX Futures Price')
axes[1].legend()
axes[2].plot(df_vm.index, df_vm['Term_Spread'], color='purple', linewidth=1.2)
axes[2].axhline(0, color='k', linestyle='--', linewidth=0.8, label='Contango/Backwardation boundary')
axes[2].axvline(event, color='black', linestyle='--', linewidth=1.5)
axes[2].set_ylabel('VX2 − VX1')
axes[2].set_xlabel('Date')
axes[2].legend(fontsize=8)
plt.tight_layout()
plt.show()
```
```{python}
#| label: vm-statistics
# Use the nearest available trading dates
pre_event = df_vm[df_vm.index < event].iloc[-1]
on_event = df_vm[df_vm.index >= event].iloc[0]
post_event = df_vm[df_vm.index > event].iloc[4] # ~1 week later
print("=== Volmageddon: Key Dates ===")
print(f"{'Metric':<22} {'Pre-event':>12} {'5 Feb':>12} {'1 Week Later':>14}")
print("-" * 62)
print(f"{'VIX':<22} {pre_event['VIX']:>12.1f} {on_event['VIX']:>12.1f} {post_event['VIX']:>14.1f}")
print(f"{'VX1 (Front Month)':<22} {pre_event['VX1']:>12.1f} {on_event['VX1']:>12.1f} {post_event['VX1']:>14.1f}")
print(f"{'Term Spread (VX2-VX1)':<22} {pre_event['Term_Spread']:>12.2f} {on_event['Term_Spread']:>12.2f} {post_event['Term_Spread']:>14.2f}")
```
::: {.callout-tip}
### Discussion 3.1
Examine how the term structure behaved on and after 5 February. Did the curve invert (move into backwardation) at the peak of the spike? How quickly did it recover to contango? Compare this to what you would expect if the market believed the volatility shock was permanent. What does the term structure tell you that the VIX level alone cannot?
:::
## Part 4: COVID-19 : March 2020
The COVID crash is the cleanest available illustration of a *liquidity-driven* crisis. In a fundamental crisis, asset prices fall because the economic outlook genuinely deteriorates. In a liquidity crisis, assets that bear no fundamental relation to one another sell off simultaneously because institutional investors : facing margin calls or redemption requests : need to raise cash quickly, selling whatever is liquid. The two mechanisms produce different cross-asset correlation patterns, and Bloomberg data lets you see the difference directly.
### Bloomberg Extraction
```
=BDH("VIX Index", "PX_LAST", "20200101", "20200430", "Days", "A")
=BDH("SPX Index", "PX_LAST", "20200101", "20200430", "Days", "A")
=BDH("LQD US Equity", "PX_LAST", "20200101", "20200430", "Days", "A")
=BDH("HYG US Equity", "PX_LAST", "20200101", "20200430", "Days", "A")
=BDH("GLD US Equity", "PX_LAST", "20200101", "20200430", "Days", "A")
=BDH("TLT US Equity", "PX_LAST", "20200101", "20200430", "Days", "A")
```
Save as `covid_2020.csv` with columns `Date`, `VIX`, `SPX`, `LQD`, `HYG`, `GLD`, `TLT`.
LQD is the iShares investment-grade corporate bond ETF, HYG is the iShares high-yield corporate bond ETF, GLD tracks gold, and TLT tracks long-dated US Treasuries : each a canonical safe-haven or diversifier in normal markets.
### Python Analysis
```{python}
#| label: covid-load
df_cv = pd.read_csv('covid_2020.csv', parse_dates=['Date'], index_col='Date')
df_cv.columns = ['VIX', 'SPX', 'LQD', 'HYG', 'GLD', 'TLT']
assets = ['SPX', 'LQD', 'HYG', 'GLD', 'TLT']
for col in assets:
df_cv[f'{col}_ret'] = df_cv[col].pct_change() * 100
df_cv['SPX_ret_raw'] = df_cv['SPX'].pct_change() * 100
df_cv['RV_21'] = df_cv['SPX_ret_raw'].rolling(21).std() * np.sqrt(252)
df_cv['VRP'] = df_cv['VIX'] - df_cv['RV_21']
ret_cols = [f'{c}_ret' for c in assets]
crash_start = pd.Timestamp('2020-02-19')
crash_trough = pd.Timestamp('2020-03-23')
```
```{python}
#| label: covid-correlations
calm = df_cv[df_cv.index < crash_start][ret_cols].dropna()
crisis = df_cv[(df_cv.index >= crash_start) & (df_cv.index <= crash_trough)][ret_cols].dropna()
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
im1 = axes[0].imshow(calm.corr(), cmap='RdYlGn', vmin=-1, vmax=1)
axes[0].set_xticks(range(len(assets)))
axes[0].set_yticks(range(len(assets)))
axes[0].set_xticklabels(assets, rotation=45)
axes[0].set_yticklabels(assets)
axes[0].set_title('Correlations: Calm Period (Jan–18 Feb 2020)')
plt.colorbar(im1, ax=axes[0])
im2 = axes[1].imshow(crisis.corr(), cmap='RdYlGn', vmin=-1, vmax=1)
axes[1].set_xticks(range(len(assets)))
axes[1].set_yticks(range(len(assets)))
axes[1].set_xticklabels(assets, rotation=45)
axes[1].set_yticklabels(assets)
axes[1].set_title('Correlations: Crisis Period (19 Feb–23 Mar 2020)')
plt.colorbar(im2, ax=axes[1])
plt.suptitle('COVID-19: Cross-Asset Correlation Breakdown', fontsize=12, y=1.02)
plt.tight_layout()
plt.show()
```
```{python}
#| label: covid-correlation-table
print("=== SPX Pairwise Correlations: Calm vs Crisis ===")
print(f"{'Asset':<8} {'Calm':>10} {'Crisis':>10} {'Change':>10}")
print("-" * 40)
for a in ['LQD', 'HYG', 'GLD', 'TLT']:
c_calm = calm['SPX_ret'].corr(calm[f'{a}_ret'])
c_crisis = crisis['SPX_ret'].corr(crisis[f'{a}_ret'])
print(f"{a:<8} {c_calm:>10.3f} {c_crisis:>10.3f} {c_crisis - c_calm:>+10.3f}")
```
```{python}
#| label: covid-timeline
fig, axes = plt.subplots(2, 1, figsize=(12, 8), sharex=True)
axes[0].plot(df_cv.index, df_cv['VIX'], color='coral', linewidth=1.5, label='VIX')
axes[0].axvspan(crash_start, crash_trough, alpha=0.15, color='red', label='Crisis window')
axes[0].legend()
axes[0].set_ylabel('VIX')
axes[0].set_title('COVID-19: VIX and Cross-Asset Returns')
for col, colour in zip(['SPX', 'GLD', 'TLT'], ['steelblue', 'gold', 'green']):
scaled = df_cv[col] / df_cv[col].iloc[0] * 100
axes[1].plot(df_cv.index, scaled, label=col, linewidth=1, color=colour)
axes[1].axvspan(crash_start, crash_trough, alpha=0.15, color='red')
axes[1].set_ylabel('Indexed Price (1 Jan 2020 = 100)')
axes[1].set_xlabel('Date')
axes[1].legend()
plt.tight_layout()
plt.show()
```
::: {.callout-tip}
### Discussion 4.1
Gold and long-dated Treasuries are canonical safe-haven assets : they are expected to appreciate when equities fall. Examine your correlation table and timeline chart. On approximately what date did this safe-haven relationship break down? What does its breakdown tell you about the nature of the selling pressure at that point? When : and why : did safe-haven behaviour resume?
:::
## Part 5: Synthesis
Having examined three distinct episodes with real Bloomberg data, you are now in a position to answer the central question the lab set out at the start: do these crises share a structural fingerprint, and is the feedback mechanism proposed in the lecture visible in each of them?
```{python}
#| label: synthesis-table
# Ensure VRP is computed for all dataframes
for df in [df_base, df_08, df_vm, df_cv]:
if 'VRP' not in df.columns:
df['VRP'] = df['VIX'] - df['RV_21']
episodes = {
'2017 Baseline': df_base,
'2008 GFC': df_08,
'2018 Volmageddon': df_vm,
'2020 COVID': df_cv,
}
print("=== Cross-Episode Comparison ===")
print(f"{'Episode':<22} {'Mean VIX':>10} {'Peak VIX':>10} {'Mean VRP':>10} {'Days VRP<0':>12}")
print("-" * 66)
for name, df in episodes.items():
vrp_clean = df['VRP'].dropna()
print(
f"{name:<22} "
f"{df['VIX'].mean():>10.1f} "
f"{df['VIX'].max():>10.1f} "
f"{vrp_clean.mean():>10.1f} "
f"{(vrp_clean < 0).mean()*100:>11.0f}%"
)
```
### Written Synthesis
Address the following questions in 250–350 words and submit with your deliverables.
**Speed of onset.** Rank the three crisis episodes by how quickly VIX escalated from its pre-event level to its peak. What does the speed of escalation tell you about the underlying mechanism in each case?
**The VRP sign reversal.** In which episodes did the volatility risk premium turn negative : that is, implied volatility fell below realised volatility? What does this mean for an investor who had been systematically harvesting the premium by selling volatility? Is this reversal a coincidence, or an intrinsic feature of the feedback mechanism?
**Fundamental versus mechanical.** Based on your data, which of the three episodes was most clearly driven by the endogenous feedback mechanism described in the lecture, rather than by deteriorating economic fundamentals? Justify your answer with specific evidence from your charts and statistics.
**Implications for GARCH-based risk management.** If you were a risk manager using GARCH-estimated VaR to set position limits, in which episode would your model have given you the least warning before the worst days? Why does this matter for the design of regulatory capital frameworks?
## Deliverables
Submit the following before the next session:
**Data summary table**
| Metric | 2017 Baseline | 2008 GFC | 2018 Volmageddon | 2020 COVID |
|--------|--------------|----------|------------------|------------|
| Mean VIX | | | | |
| Peak VIX | | | | |
| Mean VRP | | | | |
| Days VRP < 0 (%) | | | | |
**Charts** : export and label clearly:
- VIX timeline with key event dates marked (Parts 2–4)
- Term structure chart from Part 3
- Correlation heatmaps from Part 4
**Written synthesis** (250–350 words, see above)
**Connection to homework.** Compare the GARCH-estimated conditional volatility from your SPY model (Lab 4 Homework) to the VIX series extracted today. In the 2020 episode, which measure responded more quickly? What does this difference reveal about the relative nature of implied versus historical volatility as risk signals?
## References
- @danielsson2002emperor on the limits of risk modelling and endogenous risk
- @danielsson2012endogenous on procyclical amplification of volatility shocks
- @carr2009variance on variance risk premia
- @bollerslev2009expected on expected returns and the volatility risk premium