Lesson 1.6.1 · 60 min

Returns, volatility, rolling correlations and drawdown

Volatility is not a number, it is a series that changes regime, and each way of estimating it reaches a new regime with a different lag. This lesson compares the fixed window with the RiskMetrics EWMA, measures which rolling correlations are signal and which are noise, and breaks drawdown down into depth and time underwater.

Fig. — ISX volatility in 2020: EWMA with λ = 0.94 (light line) and 63-session standard deviation (grey), both with data up to the previous day
Level 1 lessons
  1. 1.1 · How markets work

    1. 1.1.1 Market participants and trading venues
    2. 1.1.2 Sessions, auctions, holidays and calendars
    3. 1.1.3 Order book: bid, ask, spread and depth
    4. 1.1.4 Order types
    5. 1.1.5 Costs, settlement, leverage and margin
  2. 1.2 · Asset classes

    1. 1.2.1 Stocks: splits, dividends and corporate actions
    2. 1.2.2 ETFs: replication, tracking error, leveraged ETFs and UCITS
    3. 1.2.3 Futures: specifications, margin, roll and contango
    4. 1.2.4 Forex: pairs, pips, lots, rollover and CFDs
    5. 1.2.5 Options: calls, puts, intrinsic value and time value
    6. 1.2.6 Crypto: spot, perpetuals, funding and custody
    7. 1.2.7 Fixed income and interest rates; CFDs and leveraged products
  3. 1.3 · Market data sources

    1. 1.3.1 Types of market data: EOD, bars, ticks, order book and reference
    2. 1.3.2 Free market data sources
    3. 1.3.3 Retail and prosumer market data APIs
    4. 1.3.4 Professional market data providers
    5. 1.3.5 Crypto and on-chain data
    6. 1.3.6 Brokers as market data sources
    7. 1.3.7 Market data licensing and terms of use
    8. 1.3.8 How to choose a market data provider
  4. 1.4 · Financial data engineering

    1. 1.4.1 Downloading over REST and websockets: pagination, rate limits and retries
    2. 1.4.2 Data cleaning: missing values, bad ticks, gaps and time zones
    3. 1.4.3 Adjusting for splits and dividends: adjusted and unadjusted prices
    4. 1.4.4 Universes without survivorship bias: delistings, mergers and symbol changes
    5. 1.4.5 Building bars: time, tick, volume, dollar and imbalance
    6. 1.4.6 Continuous futures series: roll rules, difference adjustment and ratio adjustment
    7. 1.4.7 Storage: CSV, Parquet, partitioning, DuckDB, TimescaleDB and ArcticDB
    8. 1.4.8 Reproducible pipeline: download, validation, incremental updates and quality tests
  5. 1.5 · Fundamental and macro data

    1. 1.5.1 Financial statements, ratios, XBRL and SEC EDGAR
    2. 1.5.2 Macro data, economic calendars, estimates and transcripts
  6. 1.6 · Financial exploratory analysis

    1. 1.6.1 Returns, volatility, rolling correlations and drawdown
    2. 1.6.2 Stylized facts: fat tails, clustering, autocorrelation and leverage
    3. 1.6.3 Seasonality: day of the week, month, turn of the month and intraday sessions
  7. 1.7 · Programmed technical analysis

    1. 1.7.1 Technical indicators in Python
    2. 1.7.2 Your own implementation versus TA-Lib: validating results
    3. 1.7.3 Candlestick patterns and support and resistance
    4. 1.7.4 Multi-timeframe analysis without look-ahead

Two indices, seven years

In level 0 you computed returns, the volatility of a 21-session window and the drawdown of a fictional ETF (lessons 0.5.3 and 0.7.1). This lesson starts from there and moves on to what matters for trading: how volatility changes, with what lag each estimator detects it, how far you can trust a rolling correlation and what a drawdown looks like from the inside.

The data are two fictional level 1 indices, with the real NYSE sessions from 2019 to 2025: ISX, a total return index (etf_indice_diario.csv), and FIX, a futures-based price index (fix_indice_diario.csv). Both have episodes of high volatility in 2020, 2022 and the spring of 2025.

IN [01]python
import numpy as np
import pandas as pd

isx = pd.read_csv("etf_indice_diario.csv", parse_dates=["fecha"],
                  index_col="fecha")["indice"]
fix = pd.read_csv("fix_indice_diario.csv", parse_dates=["fecha"],
                  index_col="fecha")["indice"]
px = pd.DataFrame({"ISX": isx, "FIX": fix})
r = np.log(px).diff().dropna()   # log returns
print(px.index[0].date(), "to", px.index[-1].date(), len(px), "sessions")
anual = pd.DataFrame({
    "media_%": 100 * 252 * r.mean(),
    "vol_%": 100 * np.sqrt(252) * r.std(),
    "peor_dia_%": 100 * r.min(),
    "mejor_dia_%": 100 * r.max()})
anual.round(2)
OUT [01]ISX and FIX
2019-01-02 to 2025-12-31 1760 sessions
     media_%  vol_%  peor_dia_%  mejor_dia_%
ISX    12.83  20.87       -7.95         7.84
FIX    14.55  20.07       -8.98         8.15

Both have an annual volatility of around 20%, worst days of −8% and −9%, best days of +8% and mean returns of 12.8% and 14.6% (in log terms, annualized by 252). A volatility over the whole sample says little: it is the average of very quiet years and very turbulent months. What is useful is the series.

Fixed-window volatility

The historical volatility of n sessions is the standard deviation of the last n returns, annualized by √252. The size of the window is a trade-off: short, noisy and fast; long, stable and slow.

IN [02]python
x = r["ISX"]
vol = pd.DataFrame({
    f"v{n}": x.rolling(n).std() * np.sqrt(252)
    for n in (21, 63, 252)})
(100 * vol.loc["2020-02-14":"2020-05-29"].iloc[::10]).round(1)
OUT [02]Three windows in the 2020 crash
             v21   v63  v252
fecha
2020-02-14  10.6  12.1  11.4
2020-03-02  17.5  13.2  11.7
2020-03-16  22.7  17.9  13.1
2020-03-30  27.7  23.4  15.1
2020-04-14  34.3  26.1  16.7
2020-04-28  38.2  29.8  18.4
2020-05-12  45.5  36.7  20.9
2020-05-27  37.2  38.6  21.7

In mid-February 2020 all three windows see a calm market, at around 11-12%. The 21-session window reaches 34.3% in mid-April and 45.5% in May; the 63-session one, 26.1% and 36.7%; the 252-session one, which carries a year of quiet days inside it, does not go above 21.7% over the whole period. A rectangular window weighs all its days equally: until the quiet days leave it, it underestimates the new regime.

EWMA: weight decays with age

The exponentially weighted moving average (EWMA) of squared returns gives each day a weight that decays geometrically. It is the one used by RiskMetrics, J.P. Morgan's risk methodology from the nineties, which set λ = 0.94 for daily data and λ = 0.97 for monthly data1Note 1 · J.P. Morgan/Reuters, RiskMetrics — Technical Document, 4th edition, 1996: correlations and volatilities with exponential averages and decay factors of 0.94 (daily data, “74 day cutoff 1%”) and 0.97 (monthly).:

σt2=λ σt−12+(1−λ) rt−12\sigma^2_t = \lambda\,\sigma^2_{t-1} + (1-\lambda)\,r^2_{t-1}

Yesterday's return weighs 1 − λ = 6%; the day before's, 6% × 0.94; the one from k days ago, 0.06 × 0.94^(k−1). RiskMetrics assumes a zero mean, reasonable with daily data, and that is why it uses r² instead of (r − mean)².

IN [03]python
lam = 0.94
k = np.arange(1, 253)
w = (1 - lam) * lam ** (k - 1)
print(f"Half-life: {np.log(0.5) / np.log(lam):.1f} sessions")
for n in (10, 21, 63, 74):
    print(f"Weight of the last {n}: {w[:n].sum():.1%}")
OUT [03]EWMA weights versus a 63-session window
Half-life: 11.2 sessions
Weight of the last 10: 46.1%
Weight of the last 21: 72.7%
Weight of the last 63: 98.0%
Weight of the last 74: 99.0%
Fig. — Weight of each past return in today's variance: EWMA with λ = 0.94 (bars) against a 63-session window (line)

Half of the weight is in the last eleven sessions and 99% in the last 74 (the cutoff figure that the RiskMetrics document itself gives for λ = 0.94). A 63-session window gives 1/63 = 1.6% to each day and 0 to the 64th: the day that leaves the window causes a jump in the estimate even if nothing happens today.

The recursion can be written as a loop or with ewm. With adjust=False, pandas applies the recursion exactly; with alpha = 1 − λ:

IN [04]python
def ewma_var(x, lam=0.94, var0=None):
    """EWMA variance for each day with data up to the previous day."""
    v = np.empty(len(x))
    v[0] = np.var(x[:21]) if var0 is None else var0
    for t in range(1, len(x)):
        v[t] = lam * v[t - 1] + (1 - lam) * x[t - 1] ** 2
    return v

xa = x.to_numpy()
a_mano = ewma_var(xa)
# ewm(adjust=False) includes day t: shift by one
con_ewm = (x.pow(2).ewm(alpha=0.06, adjust=False).mean()
           .shift(1).to_numpy())
dif = np.abs(np.sqrt(a_mano[300:]) - np.sqrt(con_ewm[300:]))
print(f"Maximum difference after 300 sessions: {dif.max():.2e}")
ewma = pd.Series(np.sqrt(252 * a_mano), index=x.index)
vol["ewma"] = ewma
(100 * vol.loc["2020-02-14":"2020-05-29"].iloc[::10]).round(1)
OUT [04]EWMA by hand and with ewm
Maximum difference after 300 sessions: 2.32e-11
             v21   v63  v252  ewma
fecha
2020-02-14  10.6  12.1  11.4  10.6
2020-03-02  17.5  13.2  11.7  17.4
2020-03-16  22.7  17.9  13.1  26.5
2020-03-30  27.7  23.4  15.1  34.5
2020-04-14  34.3  26.1  16.7  33.7
2020-04-28  38.2  29.8  18.4  37.4
2020-05-12  45.5  36.7  20.9  40.3
2020-05-27  37.2  38.6  21.7  37.7

The two versions match except for the initial value, whose effect disappears within a few weeks (0.94³⁰⁰ is negligible). And the difference in timing is in the table: the EWMA goes from 10.6% to 34.5% in six weeks and settles between 34% and 40% while the 21-session window keeps rising to 45.5%.

The figure at the top draws it day by day against the 63-session window, both with data up to the previous day. When does each one cross 30%?

IN [05]python
v63 = (x.rolling(63).std() * np.sqrt(252)).shift(1)
for nombre, serie in [("EWMA", ewma), ("Window 63", v63)]:
    s = serie.loc["2020"]
    print(f"{nombre:>10}: 30% on {s[s >= 0.30].index[0].date()},"
          f" maximum {s.max():.1%} on {s.idxmax().date()}")
OUT [05]The window's lag, in dates
      EWMA: 30% on 2020-03-24, maximum 51.1% on 2020-06-16
 Window 63: 30% on 2020-04-30, maximum 45.1% on 2020-06-22

The EWMA crosses 30% on March 24; the 63-session window, on April 30, five weeks later. A system that reduces exposure when volatility exceeds a threshold would have reacted more than a month apart depending on the estimator. On the way out, the same thing happens in reverse: the EWMA returns to low levels sooner and restores exposure sooner.

One detail of the cell matters more than the rest: the variance for day t uses returns up to t − 1. If volatility is used to size today's position (volatility targeting, lesson 0.7.3), the estimator cannot include today's return, which has not happened yet when the decision is made. rolling(...).std() and ewm(...).mean() include day t: in a backtest, always shift(1).

r up to t-1σ estimated at tPosition at tOutcome r at t
Mermaid source
flowchart LR
  R[r up to t-1] --> S[σ estimated at t]
  S --> P[Position at t]
  P --> G[Outcome r at t]

Which estimator forecasts better

A volatility estimator is a forecast: it says how much the price will move tomorrow. It can be evaluated like any forecast, comparing the estimated variance with the next day's squared return. Two common measures: the mean squared error and the QLIKE loss, which penalizes falling short more than overshooting and is robust to the noise in r²:

QLIKE=rt2σt2−ln⁡rt2σt2−1\text{QLIKE} = \frac{r_t^2}{\sigma_t^2} - \ln\frac{r_t^2}{\sigma_t^2} - 1

Another, more intuitive way: divide each return by the forecast volatility. If the estimator is good, the standardized returns have a standard deviation close to 1, the lowest possible kurtosis and a share of days below −2.33 standard deviations close to what the normal distribution expects (1%).

IN [06]python
prev = pd.DataFrame({
    "v21": x.rolling(21).std(), "v63": x.rolling(63).std(),
    "v252": x.rolling(252).std(),
    "ewma94": np.sqrt(x.pow(2).ewm(alpha=0.06, adjust=False).mean()),
    "ewma97": np.sqrt(x.pow(2).ewm(alpha=0.03, adjust=False).mean()),
}).shift(1)                      # forecast for the next day
d = prev.join(x.rename("r")).dropna()
filas = {}
for c in prev:
    s2, r2 = d[c] ** 2, d["r"] ** 2
    z = d["r"] / d[c]
    filas[c] = {"qlike": (r2 / s2 - np.log(r2 / s2) - 1).mean(),
                "z_desv": z.std(), "z_curtosis": z.kurt(),
                "bajo_-2,33_%": 100 * (z < -2.326).mean()}
pd.DataFrame(filas).T.round(3)
OUT [06]Five estimators evaluated as forecasts
        qlike  z_desv  z_curtosis  bajo_-2,33_%
v21     1.818   1.195      10.664         2.190
v63     1.827   1.122       9.979         2.057
v252    2.180   1.193      11.176         2.588
ewma94  1.696   1.088       7.326         1.924
ewma97  1.751   1.061       9.304         1.924

The EWMA with λ = 0.94 has the lowest QLIKE (1.696) and the lowest kurtosis (7.3 versus 10-11 for the windows); the one with λ = 0.97 brings the standard deviation closest to 1 (1.06 versus 1.09), and the two tie on days below −2.33 (1.9%). The windows lag behind on everything. None gets down to 1% of days below −2.33: the tails are still fatter than the normal's even after standardizing (lesson 1.6.2). The 252-session window is the worst on QLIKE, kurtosis and days below −2.33: a “last year” volatility is a poor forecast of tomorrow's.

Rolling correlations: signal and noise

The correlation between two assets also changes, and a diversified portfolio depends on it. The 63-session rolling correlation between ISX and FIX:

IN [07]python
rc = r["ISX"].rolling(63).corr(r["FIX"])
banda = 1.96 / np.sqrt(63)
print(f"Full-sample correlation: {r.corr().iloc[0, 1]:.3f}")
print(rc.describe().round(3).to_string())
print(f"95% band if the true correlation is 0: ±{banda:.3f}")
print(f"Outside the band: {(rc.abs() > banda).mean():.1%}")
print("Maximum:", rc.idxmax().date(), round(rc.max(), 3))
OUT [07]63-session rolling correlation
Full-sample correlation: 0.066
count    1697.000
mean        0.010
std         0.155
min        -0.438
25%        -0.080
50%        -0.006
75%         0.099
max         0.511
95% band if the true correlation is 0: ±0.247
Outside the band: 10.1%
Maximum: 2025-04-23 0.511
Fig. — 63-session rolling correlation between ISX and FIX daily returns, 2019–2025 (last value of each week), with the 95% band for a zero true correlation

The full-sample correlation is almost zero and the rolling one oscillates from −0.44 to +0.51. Is the relationship between the two indices changing, or is it noise? With 63 data points and a true correlation of zero, 95% of the estimates fall within ±0.247 (standard error ≈ 1/√n). Here it falls outside 10.1% of the time: double the 5% that chance would give, but far from what would be needed to read each peak as a change. The maximum, 0.51, is on April 23, 2025, in the middle of the fall of both indices, and the March 2020 peak reaches almost 0.50: the two stress episodes are the two peaks.

To check how much a rolling correlation oscillates with no relationship at all, it is enough to simulate it:

IN [08]python
rng = np.random.default_rng(7)
ind = pd.DataFrame(rng.standard_normal((len(r), 2)))
rc0 = ind[0].rolling(63).corr(ind[1])
print(f"Minimum {rc0.min():.3f}, maximum {rc0.max():.3f}")
print(f"Outside the band: {(rc0.abs() > banda).mean():.1%}")
OUT [08]Two independent series of the same length
Minimum -0.255, maximum 0.300
Outside the band: 1.2%

Two series with no relationship at all produce a rolling correlation that ranges from −0.26 to +0.30. In this simulation only 1.2% of the time falls outside the band: with overlapping windows, the values on consecutive days are not independent and the fraction for a single path varies a lot around 5%. Every rolling correlation chart has to be read with that reference next to it.

And the stress periods? In real markets it is often observed that correlations between risk assets rise during selloffs, exactly when diversification would be most needed. There is a trace of that in this data, but it comes from frequency, not from day-to-day moves:

IN [09]python
for regla, nombre in [("W-FRI", "weekly"), ("ME", "monthly"),
                      ("QE", "quarterly")]:
    rr = np.log(px.resample(regla).last()).diff().dropna()
    print(f"{nombre:>10}: {rr.corr().iloc[0, 1]:.3f}"
          f"  ({len(rr)} data points)")
vol21 = r.rolling(21).std()
print(f"Correlation of the 21-session volatilities: "
      f"{vol21['ISX'].corr(vol21['FIX']):.3f}")
OUT [09]Correlation depends on frequency
    weekly: 0.156  (365 data points)
   monthly: 0.453  (83 data points)
 quarterly: 0.819  (27 data points)
Correlation of the 21-session volatilities: 0.650

At daily frequency the correlation is 0.06; weekly, 0.16; monthly, 0.45, and quarterly, 0.82. The two indices share the big calendar trends (the falls of 2020, 2022 and 2025 and the subsequent rallies), but not the moves of each day, which in this data are independent. When aggregating, the daily noise cancels out and the common part remains. And their volatilities do move together (0.65): the high-volatility regimes are the same. For a portfolio, the consequence is clear: the diversification measured with daily data may not exist at the horizon that really matters, that of months.

Drawdown and time underwater

Each day's drawdown is the fall from the previous peak:

DDt=Ptmax⁡s≤tPs−1DD_t = \frac{P_t}{\max_{s \le t} P_s} - 1

Its minimum is the maximum drawdown, the figure everybody quotes. But a drawdown has two dimensions: how far it falls and how long it lasts. An underwater episode starts on the day the price falls below its peak and ends on the day it exceeds it again.

falls from the peakstays belowexceeds the peakAtPeakUnderwater
Mermaid source
stateDiagram-v2
  [*] --> AtPeak
  AtPeak --> Underwater: falls from the peak
  Underwater --> Underwater: stays below
  Underwater --> AtPeak: exceeds the peak
IN [10]python
def episodios(precio):
    pico = precio.cummax()
    dd = precio / pico - 1
    bajo = dd < 0
    grupo = (~bajo).cumsum()         # one number per episode
    filas = []
    for _, e in dd[bajo].groupby(grupo[bajo]):
        fin = e.index[-1]
        sig = precio.index.searchsorted(fin, side="right")
        recuperado = sig < len(precio)
        filas.append({
            "inicio": e.index[0].date(),
            "minimo": e.idxmin().date(),
            "recupera": precio.index[sig].date()
                        if recuperado else None,
            "profundidad_%": 100 * e.min(),
            "sesiones_hasta_minimo": e.index.get_loc(e.idxmin()) + 1,
            "sesiones_bajo_agua": len(e)})
    return pd.DataFrame(filas), dd

ep, dd_isx = episodios(px["ISX"])
print(f"Sessions underwater: {(dd_isx < 0).mean():.1%}")
print(f"Episodes: {len(ep)}")
ep.sort_values("profundidad_%").head(5).round(1)
OUT [10]ISX underwater episodes
Sessions underwater: 92.9%
Episodes: 54
        inicio      minimo    recupera  profundidad_%  sesiones_hasta_minimo  sesiones_bajo_agua
22  2020-02-20  2020-04-21  2021-03-29          -43.2                     43                 278
31  2022-02-15  2022-05-25  2023-04-17          -28.9                     70                 292
41  2025-02-20  2025-04-04  2025-06-25          -28.0                     32                  86
35  2023-07-17  2024-01-02  2024-10-03          -18.6                    118                 307
36  2024-10-09  2024-11-26  2025-01-13           -7.2                     35                  64
Fig. — ISX drawdown, 2019–2025 (worst value of each week): the deepest episode is not the longest

92.9% of ISX sessions are below the previous peak: you are almost always “underwater”, even if only slightly. What sets an episode apart is its depth and its duration, and they do not go together:

IN [11]python
ep.sort_values("sesiones_bajo_agua", ascending=False).head(4)[
    ["inicio", "recupera", "profundidad_%",
     "sesiones_hasta_minimo", "sesiones_bajo_agua"]].round(1)
OUT [11]The longest are not the deepest
        inicio    recupera  profundidad_%  sesiones_hasta_minimo  sesiones_bajo_agua
35  2023-07-17  2024-10-03          -18.6                    118                 307
31  2022-02-15  2023-04-17          -28.9                     70                 292
22  2020-02-20  2021-03-29          -43.2                     43                 278
41  2025-02-20  2025-06-25          -28.0                     32                  86

The 2020 crash is the deepest (−43.2%) but takes 278 sessions to recover, fewer than the episode that starts in July 2023, only −18.6% deep and with 307 sessions underwater: a moderate fall followed by a slow recovery. The 2022 fall lasts 292. For someone following a strategy, fourteen months without a new high weigh as much as a big fall: that is when systems get abandoned. That is why the level 2 risk metrics (lesson 2.6.1) include drawdown duration and the Ulcer Index, which combines depth and duration.

What this changes in a system

  • Volatility with shift(1). Every estimate that feeds a decision on day t uses data up to t − 1. With pandas' rolling and ewm, day t is inside: shift it.
  • Choose the estimator by its use. For sizing positions day by day, a fast one (EWMA, λ = 0.94, or a short window); for comparing strategies or setting risk limits, a slow and stable one.
  • No correlation without its band. With n data points, the noise is on the order of ±2/√n. And measure the correlation at the frequency of your horizon, not just daily.
  • Depth and duration. Report the maximum drawdown and the longest underwater episode. They are different risks.

Exercises

  1. Repeat the estimator evaluation with FIX and with λ = 0.90, 0.94 and 0.97. Which λ gives the lowest QLIKE for each index?
Solution
IN [12]python
def qlike(serie, lam):
    s2 = serie.pow(2).ewm(alpha=1 - lam, adjust=False).mean()
    dd = pd.DataFrame({"s2": s2.shift(1),
                       "r2": serie.pow(2)}).iloc[63:]
    dd = dd[dd["r2"] > 0]   # days with no change: ln(0)
    q = dd["r2"] / dd["s2"]
    return (q - np.log(q) - 1).mean()

tabla = pd.DataFrame({ind: {lam: qlike(r[ind], lam)
                            for lam in (0.90, 0.94, 0.97)}
                      for ind in ["ISX", "FIX"]})
tabla.round(4)
OUT [12]Optimal λ by index
         ISX     FIX
0.90  1.6955  1.6794
0.94  1.6973  1.6746
0.97  1.7454  1.7173

The differences are small and the optimal λ depends on the index: in ISX, 0.90 wins by two thousandths; in FIX, 0.94. With seven years of a single asset, choosing λ by QLIKE is an in-sample optimization: RiskMetrics' 0.94 is a reasonable default precisely because it was not fitted to your data.

  1. Compute the EWMA correlation (λ = 0.94) between ISX and FIX and compare it with the 63-session window one: which oscillates more?
Solution
IN [13]python
a = 0.06
cov = (r["ISX"] * r["FIX"]).ewm(alpha=a, adjust=False).mean()
var_i = r["ISX"].pow(2).ewm(alpha=a, adjust=False).mean()
var_f = r["FIX"].pow(2).ewm(alpha=a, adjust=False).mean()
corr_ewma = (cov / np.sqrt(var_i * var_f)).iloc[63:]
pd.DataFrame({"ventana_63": rc.iloc[63:].describe(),
              "ewma_094": corr_ewma.describe()}).round(3)
OUT [13]EWMA correlation
       ventana_63  ewma_094
count    1696.000  1696.000
mean        0.010     0.020
std         0.155     0.182
min        -0.438    -0.517
25%        -0.080    -0.083
50%        -0.006     0.006
75%         0.099     0.110
max         0.511     0.699

The EWMA oscillates more: its 11-session half-life is equivalent to a window much shorter than 63, and its sampling noise is larger. It is the other side of reacting sooner. The first 63 sessions are discarded because the recursion starts from a single data point and gives correlations of ±1 at the beginning.

  1. One strategy has a maximum drawdown of −15% and another of −25%. What else do you need to know before saying which one is less risky?
Solution

The duration of their underwater episodes (the longest and the typical one), the volatility of each (a −15% with a 5% volatility is a three-standard-deviation event; a −25% with 25%, an ordinary one) and how many years the history covers: the maximum drawdown grows with the length of the sample. It also matters whether the drawdown comes from a single crash or from a slow bleed, because they are managed differently (stops versus a review of the system).

Footnotes

  1. J.P. Morgan/Reuters, RiskMetrics — Technical Document, 4th edition, 1996: correlations and volatilities with exponential averages and decay factors of 0.94 (daily data, “74 day cutoff 1%”) and 0.97 (monthly). ↩

Each answer is folded under its question

Self-assessment

  1. With λ = 0.94, what weight does yesterday's return have in the EWMA?

    • A94%
    • B1/63, as in a 63-session window
    • CIt depends on the level of volatility
    • D6%: (1 − λ)
    Show answer

    Answer D. 6%: (1 − λ)

    σ²(t) = λ σ²(t−1) + (1 − λ) r²(t−1): the latest return weighs 1 − λ = 6%, the one before 6% × 0.94, and so on. The half-life of the weight is ln 0.5 / ln 0.94 ≈ 11.2 sessions.

  2. In March 2020, why does the 63-session window volatility lag behind the EWMA?

    • ABecause it is computed incorrectly
    • BBecause it uses simple returns
    • CBecause it weighs all 63 days equally: the quiet days of January and February weigh as much as those of the crash
    • DBecause the EWMA uses future information
    Show answer

    Answer C. Because it weighs all 63 days equally: the quiet days of January and February weigh as much as those of the crash

    A rectangular window spreads the weight evenly. Until the quiet days leave the window, the estimator underestimates the new regime; the EWMA gives more weight to recent days and reacts sooner. Neither uses the day it estimates if it is shifted by one day.

  3. The 63-session rolling correlation between two indices oscillates between −0.44 and +0.51 with a mean of 0.01. What is most likely?

    • AThe relationship between the two indices changes sign every few months
    • BMuch of the oscillation is sampling noise: with 63 data points and zero correlation, 95% of the estimates fall within ±0.25
    • CThere is a mistake in the calculation
    • DA 5-session window should be used
    Show answer

    Answer B. Much of the oscillation is sampling noise: with 63 data points and zero correlation, 95% of the estimates fall within ±0.25

    The standard error of a correlation close to zero with n data points is approximately 1/√n. Before interpreting a change in correlation, compare it with that band.

  4. The daily correlation of ISX and FIX is 0.06 and the quarterly one is 0.82. How can that be?

    • ABecause the two indices share the big trends (the falls of 2020, 2022 and 2025) but not the daily moves: at low frequencies the common part dominates
    • BBecause of a rounding error
    • CBecause quarterly correlation is always higher
    • DBecause there are fewer holidays
    Show answer

    Answer A. Because the two indices share the big trends (the falls of 2020, 2022 and 2025) but not the daily moves: at low frequencies the common part dominates

    Correlation depends on frequency. What dominates the day-to-day moves of these indices is independent noise; when aggregating, that noise cancels out and the common trend remains. With 27 quarters, moreover, the estimate is very imprecise.

  5. ISX's deepest drawdown is −43.2%, but its longest episode underwater is not that one. What does each measure?

    • AThe same thing
    • BDepth measures how much is lost from the peak; duration, how long it takes to get back to it. A fast fall with a fast rebound can last less than a gentle fall with no rebound
    • CDuration only counts losing days
    • DDrawdown cannot be measured in days
    Show answer

    Answer B. Depth measures how much is lost from the peak; duration, how long it takes to get back to it. A fast fall with a fast rebound can last less than a gentle fall with no rebound

    In ISX, the 2020 fall recovers in 278 sessions; the 2023-2024 episode, only −18.6% deep, lasts 307. For whoever lives through the strategy, the two dimensions hurt in different ways.

Key points

  • A fixed window weighs all its days equally and arrives late to new regimes; the EWMA with λ = 0.94 (RiskMetrics, daily data) has a half-life of about 11 sessions.
  • To use a volatility in today's sizing, it is estimated with data up to yesterday: shift(1). An estimator is evaluated as a forecast, with QLIKE or with standardized returns.
  • A rolling correlation of n data points has noise of around ±2/√n: with 63 sessions, ±0.25. Before reading a change, compare it with that band.
  • Correlation depends on frequency: ISX and FIX have 0.06 at daily frequency and 0.82 at quarterly frequency.
  • Drawdown has depth and duration. In ISX, the deepest episode (−43.2%) is not the longest (307 sessions underwater).

Resources

Documentation

Data