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