Knowing your P&L is only half the story. You also need to know how volatile your portfolio is and how far it has fallen from its peak. This part automates volatility and drawdown monitoring: rolling 20-day and 60-day volatility, current drawdown, and VIX as a macro risk gauge. These metrics tell you when your portfolio is in stress and when to tighten risk controls.

ℹ️ Not financial advice. Volatility and drawdown are statistical measures, not predictions. Past volatility does not predict future returns.

Key Metrics Explained

Metric What it measures
Rolling Volatility Standard deviation of daily returns, annualized. 20-day captures recent regime, 60-day smooths noise. A spike from 12% to 28% is a signal.
Current Drawdown Current decline from portfolio all-time high. Describes how painful the drop is right now.
Max Drawdown Largest peak-to-trough decline over a period. Historical worst case.
VIX as Gauge Implied volatility of S&P 500. VIX >25 = tighten stops. VIX >35 = consider reducing. A risk input, not a trading signal.

Volatility & Drawdown Calculations

Load your positions, calculate daily returns, and compute rolling volatility and drawdown:

import yfinance as yf
import pandas as pd
import numpy as np

def portfolio_returns(positions: list, period: str = "6mo") -> pd.Series:
    tickers = [p["ticker"] for p in positions]
    prices = yf.download(tickers, period=period, auto_adjust=True, progress=False)["Close"]
    total_val = sum(p["shares"] * float(prices[p["ticker"]].iloc[-1]) for p in positions)
    weights = {p["ticker"]: (p["shares"] * float(prices[p["ticker"]].iloc[-1])) / total_val
               for p in positions}
    return sum(prices[t].pct_change() * w for t, w in weights.items()).dropna()

def rolling_volatility(returns: pd.Series, windows: list = [20, 60]) -> dict:
    result = {}
    for w in windows:
        vol = returns.rolling(w).std() * np.sqrt(252)
        result[f"vol_{w}d_pct"] = round(float(vol.iloc[-1]) * 100, 2)
        result[f"vol_{w}d_avg_pct"] = round(float(vol.mean()) * 100, 2)
    return result

def drawdown_analysis(returns: pd.Series) -> dict:
    cumulative = (1 + returns).cumprod()
    peak = cumulative.expanding().max()
    drawdown = (cumulative - peak) / peak
    return {
        "current_drawdown_pct": round(float(drawdown.iloc[-1]) * 100, 2),
        "max_drawdown_pct": round(float(drawdown.min()) * 100, 2),
        "days_underwater": int((drawdown < -0.01).sum()),
        "at_all_time_high": bool(drawdown.iloc[-1] > -0.01),
    }

def get_vix() -> dict:
    hist = yf.Ticker("^VIX").history(period="5d")
    current = round(float(hist["Close"].iloc[-1]), 2)
    prior = round(float(hist["Close"].iloc[-2]), 2)
    if current < 15: regime, note = "low", "Complacency zone — review stop-losses"
    elif current < 25: regime, note = "normal", "Normal volatility environment"
    elif current < 35: regime, note = "elevated", "Elevated risk — tighten alerts"
    else: regime, note = "extreme", "Extreme stress — portfolio risk mode"
    return {"vix": current, "prior": prior, "change": round(current - prior, 2),
            "regime": regime, "note": note}

Automated Monitoring

Set up HEARTBEAT to run at 6 PM ET (after market close) to monitor volatility and drawdown:

name: volatility_drawdown_monitor
schedule: "0 18 * * 1-5"
steps:
  - load_positions:
      file: positions.yaml
  - fetch_prices:
      period: "6mo"
  - calculate_returns: {}
  - rolling_volatility:
      windows: [20, 60]
  - drawdown_analysis: {}
  - get_vix: {}
  - alert_if:
      conditions:
        - metric: current_drawdown_pct
          threshold: -10
          message: "Portfolio drawdown exceeds 10%"
        - metric: vol_20d_pct
          threshold: 25
          message: "20-day vol above 25% annualized"
        - metric: vix
          threshold: 30
          message: "VIX above 30 — elevated market stress"
  - notify:
      subject: "📉 Risk Brief: Vol {{ vol_20d }}% | DD {{ drawdown }}% | VIX {{ vix }}"

Frequently Asked Questions

Part 2 FAQs

What's normal annualized portfolio volatility?
A diversified equity portfolio typically runs 12–18% annualized in normal markets. Individual stocks can run 25–60%+.
How is drawdown different from a loss?
Drawdown measures decline from peak, not from cost basis. You can be up 40% overall but in a 15% drawdown from your portfolio's highest point.
Should I use 20-day or 60-day vol?
Both — 20-day changes faster and catches regime shifts, 60-day smooths noise. Comparing them reveals whether current vol is elevated vs trend.

Next: Move to Part 3 to analyze correlation and concentration risk — Correlation & Concentration Risk.