Lab 4 In-Class: Forensic Volatility Analysis

Three Crisis Episodes at the Bloomberg Terminal

ImportantIn-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)

1 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 (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.

2 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.

3 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.

3.1 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.

3.2 Python Analysis

Show code
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
Show code
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}")
TipDiscussion 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?

4 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.

4.1 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.

4.2 Python Analysis

Show code
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')
Show code
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()
Show code
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}%")
TipDiscussion 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?

5 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.

5.1 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.

5.2 Python Analysis

Show code
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')
Show code
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()
Show code
# 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}")
TipDiscussion 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?

6 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.

6.1 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.

6.2 Python Analysis

Show code
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')
Show code
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()
Show code
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}")
Show code
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()
TipDiscussion 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?

7 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?

Show code
# 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}%"
    )

7.1 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?

8 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?

9 References

  • Danielsson (2002) on the limits of risk modelling and endogenous risk
  • Danielsson, Shin, and Zigrand (2012) on procyclical amplification of volatility shocks
  • Carr and Wu (2009) on variance risk premia
  • Bollerslev, Tauchen, and Zhou (2009) on expected returns and the volatility risk premium

References

Bollerslev, Tim, George Tauchen, and Hao Zhou. 2009. “Expected Stock Returns and Variance Risk Premia.” Review of Financial Studies 22 (11): 4463–92. https://doi.org/10.1093/rfs/hhp008.
Carr, Peter, and Liuren Wu. 2009. “Variance Risk Premiums.” Review of Financial Studies 22 (3): 1311–41. https://doi.org/10.1093/rfs/hhn038.
Danielsson, Jón. 2002. “The Emperor Has No Clothes: Limits to Risk Modelling.” Journal of Banking & Finance 26 (7): 1273–96. https://doi.org/10.1016/S0378-4266(02)00263-7.
Danielsson, Jón, Hyun Song Shin, and Jean-Pierre Zigrand. 2012. “Endogenous and Systemic Risk.” In Quantifying Systemic Risk, edited by Joseph G. Haubrich and Andrew W. Lo, 73–94. University of Chicago Press.