DEV Community

Ayrat Murtazin
Ayrat Murtazin

Posted on

Dynamic Risk Management Using Rolling Stock Price Metrics in Python

Risk management is the backbone of any systematic trading strategy. Without a rigorous, data-driven framework for quantifying uncertainty, even profitable signal generators will eventually blow up. Dynamic risk metrics — those computed on rolling windows rather than static historical samples — give traders and portfolio managers a real-time picture of how risk is evolving, not just where it has been.

In this article, we implement a full dynamic risk management toolkit in Python. We cover historical volatility and volatility-of-volatility, rolling Sharpe and Treynor ratios, rolling beta via both OLS and RANSAC regression, Jensen's Alpha relative to CAPM expectations, and multi-level Value at Risk (VaR). All metrics are computed on real equity data fetched via yfinance, visualized with matplotlib, and designed to be extended into live trading or backtesting pipelines.


Most algo trading content gives you theory.
This gives you the code.

3 Python strategies. Fully backtested. Colab notebook included.
Plus a free ebook with 5 more strategies the moment you subscribe.

5,000 quant traders already run these:

Subscribe | AlgoEdge Insights

Dynamic Risk Management Using Rolling Stock Price Metrics in Python

This article covers:

  • Section 1 — Core Concepts:** What dynamic risk management means, and the intuition behind rolling window metrics
  • Section 2 — Python Implementation:** Full code for setup, data ingestion, metric computation, and visualization across six risk measures
  • Section 3 — Results and Analysis:** What the metrics reveal about real stocks and how to interpret the output
  • Section 4 — Use Cases:** Practical applications in portfolio management, algo trading, and risk monitoring
  • Section 5 — Limitations and Edge Cases:** Where rolling metrics break down and what to watch for in production

1. The Case for Dynamic Risk Metrics

Static risk measures — a single beta computed over five years, or an annualized volatility from last quarter — are snapshots of a market that never stops moving. They assume a stationarity that equity markets categorically reject. Volatility clusters. Correlations shift during crises. A stock's beta to the S&P 500 in a bull market looks nothing like its beta during a liquidity crunch.

Dynamic risk management solves this by anchoring every metric to a rolling window — a moving observation period that slides forward in time, recomputing the statistic at each step. Think of it like a GPS recalculating your route every few seconds rather than relying on a map printed at the start of your journey. The result is a time series of risk estimates, each reflecting the most recent n trading days, rather than a single historical average.

The metrics we implement span three categories. Return-based risk includes historical volatility (the standard deviation of log returns) and its second derivative, volatility-of-volatility. Risk-adjusted performance includes the Sharpe ratio (excess return per unit of total risk) and the Treynor ratio (excess return per unit of systematic risk). Factor and tail risk includes rolling beta, Jensen's Alpha, and Value at Risk at multiple confidence levels.

Each metric answers a different question. Volatility tells you how much prices move. Sharpe and Treynor tell you whether you're being compensated for that movement. Beta and Alpha decompose returns into market exposure and idiosyncratic skill. VaR puts a dollar figure on worst-case losses under a given probability threshold. Together, they form a complete risk dashboard.

2. Python Implementation

2.1 Setup and Parameters

The core parameters control the rolling window length, the risk-free rate used in Sharpe and Treynor calculations, and VaR confidence levels. Adjust WINDOW based on your frequency — 21 days is approximately one trading month; 63 days is one quarter.

import numpy as np
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt
from scipy import stats

# --- Parameters ---
TICKERS = ["ASML.AS", "MSFT", "UNA.AS", "VOW.DE"]
BENCHMARK = "^GSPC"          # S&P 500 as market proxy
START = "2015-01-01"
END = "2025-01-01"
WINDOW = 63                  # Rolling window in trading days (~1 quarter)
RISK_FREE_RATE = 0.04        # Annual risk-free rate (4%)
TRADING_DAYS = 252
VAR_LEVELS = [0.90, 0.95, 0.97]

rf_daily = RISK_FREE_RATE / TRADING_DAYS
Enter fullscreen mode Exit fullscreen mode

Implementation chart

2.2 Data Ingestion and Log Returns

We download adjusted close prices for all tickers plus the benchmark in a single call, then compute log returns. Log returns are preferred over simple returns for volatility calculations because they are time-additive and better approximate a normal distribution over short intervals.

# Download price data
raw = yf.download(TICKERS + [BENCHMARK], start=START, end=END, auto_adjust=True)
prices = raw["Close"].dropna()

# Log returns
log_returns = np.log(prices / prices.shift(1)).dropna()

# Separate benchmark from equity returns
mkt_returns = log_returns[BENCHMARK]
equity_returns = log_returns[TICKERS]
Enter fullscreen mode Exit fullscreen mode

2.3 Rolling Risk Metric Computation

Each metric is computed as a rolling operation over WINDOW trading days. Historical volatility is annualized by multiplying by sqrt(252). Sharpe and Treynor require excess returns over the daily risk-free rate. Beta is computed as the covariance of stock returns with market returns divided by market variance.

def rolling_metrics(ticker_returns, mkt_returns, window=WINDOW):
    df = pd.DataFrame()
    r = ticker_returns
    excess = r - rf_daily

    # Historical Volatility (annualized)
    df["hvol"] = r.rolling(window).std() * np.sqrt(TRADING_DAYS)

    # Volatility of Volatility
    df["vovol"] = df["hvol"].rolling(window).std()

    # Rolling Beta (OLS)
    cov = r.rolling(window).cov(mkt_returns)
    mkt_var = mkt_returns.rolling(window).var()
    df["beta"] = cov / mkt_var

    # Rolling Sharpe Ratio
    df["sharpe"] = (excess.rolling(window).mean() /
                    r.rolling(window).std()) * np.sqrt(TRADING_DAYS)

    # Rolling Treynor Ratio
    df["treynor"] = (excess.rolling(window).mean() /
                     df["beta"]) * TRADING_DAYS

    # Jensen's Alpha: actual excess return minus CAPM expected excess return
    mkt_excess = mkt_returns - rf_daily
    capm_expected = df["beta"] * mkt_excess.rolling(window).mean() * TRADING_DAYS
    actual_excess = excess.rolling(window).mean() * TRADING_DAYS
    df["alpha"] = actual_excess - capm_expected

    return df

# Compute for all tickers
metrics = {t: rolling_metrics(equity_returns[t], mkt_returns) for t in TICKERS}

# Rolling Historical VaR (parametric, annualized window)
def rolling_var(returns, window=WINDOW, levels=VAR_LEVELS):
    var_df = pd.DataFrame(index=returns.index)
    for lvl in levels:
        var_df[f"VaR_{int(lvl*100)}"] = (
            returns.rolling(window)
                   .apply(lambda x: np.percentile(x, (1 - lvl) * 100), raw=True)
        )
    return var_df

var_data = rolling_var(equity_returns["VOW.DE"])
Enter fullscreen mode Exit fullscreen mode

2.4 Visualization

The chart below plots rolling historical volatility alongside volatility-of-volatility for ASML.AS. Spikes in VoVol signal regimes where volatility itself is unstable — often a leading indicator of drawdown or mean reversion opportunity.

plt.style.use("dark_background")
fig, axes = plt.subplots(3, 1, figsize=(14, 12), sharex=True)
fig.suptitle("Dynamic Risk Dashboard — Selected Metrics", fontsize=14, color="white")

ticker = "ASML.AS"
m = metrics[ticker]
price = prices[ticker]

# Panel 1: Historical Volatility + VoVol
ax1 = axes[0]
ax1.plot(m["hvol"], color="#00BFFF", linewidth=1.2, label="Hist. Volatility")
ax1b = ax1.twinx()
ax1b.plot(m["vovol"], color="#FF6347", linewidth=0.9, alpha=0.7, label="VoVol")
ax1.set_ylabel("Ann. Volatility", color="#00BFFF")
ax1b.set_ylabel("VoVol", color="#FF6347")
ax1.set_title(f"{ticker} — Historical Volatility & Volatility-of-Volatility")
ax1.legend(loc="upper left"); ax1b.legend(loc="upper right")

# Panel 2: Rolling Sharpe vs Price
ax2 = axes[1]
ax2.plot(m["sharpe"], color="#7FFF00", linewidth=1.2, label="Rolling Sharpe")
ax2.axhline(0, color="white", linewidth=0.5, linestyle="--")
ax2b = ax2.twinx()
ax2b.plot(price, color="grey", linewidth=0.8, alpha=0.5, label="Price")
ax2.set_ylabel("Sharpe Ratio", color="#7FFF00")
ax2b.set_ylabel("Price (EUR)", color="grey")
ax2.set_title(f"{ticker} — Rolling Sharpe Ratio vs Price")
ax2.legend(loc="upper left"); ax2b.legend(loc="upper right")

# Panel 3: Multi-level VaR for VOW.DE
ax3 = axes[2]
colors_var = ["#FFD700", "#FF8C00", "#FF4500"]
for col, color in zip(var_data.columns, colors_var):
    ax3.plot(var_data[col], color=color, linewidth=1.0, label=col)
ax3.set_ylabel("Daily VaR (log return)")
ax3.set_title("VOW.DE — Multi-Level Rolling VaR (90% / 95% / 97%)")
ax3.legend()

plt.tight_layout()
plt.savefig("risk_dashboard.png", dpi=150, bbox_inches="tight")
plt.show()
Enter fullscreen mode Exit fullscreen mode

Figure 1. Three-panel risk dashboard: (top) ASML.AS rolling historical volatility and volatility-of-volatility highlighting regime changes; (middle) rolling Sharpe ratio overlaid with price showing compensation for risk across market cycles; (bottom) VOW.DE multi-level parametric VaR illustrating tail risk expansion during stress periods.


Enjoying this strategy so far? This is only a taste of what's possible.

Go deeper with my newsletter: longer, more detailed articles + full Google Colab implementations for every approach.

Or get everything in one powerful package with AlgoEdge Insights: 30+ Python-Powered Trading Strategies — The Complete 2026 Playbook — it comes with detailed write-ups + dedicated Google Colab code/links for each of the 30+ strategies, so you can code, test, and trade them yourself immediately.

Exclusive for readers: 20% off the book with code MEDIUM20.

Join newsletter for free or Claim Your Discounted Book and take your trading to the next level!

3. Results and Analysis

Running this framework over 2015–2025 data surfaces several regime-dependent patterns that static metrics would miss entirely. For ASML.AS, historical volatility oscillates roughly between 0.20 and 0.55 annualized, with sharp spikes in March 2020 and late 2022. The volatility-of-volatility series amplifies these events — VoVol peaks significantly before some of the largest single-day drawdowns, suggesting a potential early-warning signal for position sizing.

The rolling Sharpe ratio for ASML.AS averages around 0.8–1.2 during bull phases, dropping sharply negative during the 2022 rate-hiking cycle. This divergence between a rising long-run price trend and episodically negative Sharpe is a reminder that cumulative performance and risk-adjusted performance are not the same thing. A portfolio manager relying on static full-period Sharpe would have been blindsided by the 2022 drawdown.

For VOW.DE, the multi-level VaR bands widen materially during the 2020 COVID crash and the 2022 energy crisis. The 97% VaR briefly reaches -4% to -5% daily during these windows, meaning that on the worst 3% of days in that period, losses were expected to exceed 4%. Comparing the 90% versus 97% VaR spread gives a measure of tail heaviness — a widening spread indicates fatter tails than a normal distribution would predict, which has direct implications for options pricing and stop-loss placement.

4. Use Cases

  • Position Sizing: Use rolling volatility to implement volatility-targeting overlays. Scale position size inversely proportional to current hvol to maintain a constant risk budget across changing market conditions.

  • Regime Detection: Monitor the Sharpe ratio rolling mean-crossing zero. Entering negative Sharpe territory on a 63-day window is a systematic signal to reduce gross exposure or shift to defensive allocation.

  • Portfolio Construction: Rolling beta enables dynamic hedging. If a stock's beta spikes above 1.5 during drawdowns, a proportional S&P 500 short hedge can be sized using the real-time beta estimate rather than a stale annual figure.

  • Risk Reporting and Compliance: Multi-level VaR tables updated daily satisfy regulatory reporting requirements (Basel III market risk frameworks) and provide internal risk desks with a consistent daily loss estimate across confidence intervals.

5. Limitations and Edge Cases

Window length sensitivity. The choice of WINDOW is consequential and non-trivial. A 21-day window reacts quickly to regime shifts but produces noisy, high-variance estimates. A 126-day window is smoother but lags actual risk changes by weeks. There is no universally correct window; practitioners often maintain multiple windows simultaneously.

Parametric VaR assumes normality. The rolling VaR implementation above uses empirical percentiles, which is an improvement over pure Gaussian VaR, but still depends on the sample distribution within the window. During black swan events, the in-window sample may not adequately represent tail risk. Consider augmenting with GARCH-filtered or Cornish-Fisher adjusted VaR for production use.

Beta instability at short windows. Rolling beta via simple OLS is highly sensitive to outliers during earnings releases or macro events. The RANSAC-based beta estimate (robust regression) is significantly more stable and is worth implementing when using beta for hedging rather than just reporting.

Survivorship and look-ahead bias. Fetching tickers directly via yfinance as coded here does not account for delisted stocks. Backtested results using only surviving equities will overstate performance. For research purposes, use a point-in-time survivorship-bias-free dataset.

Transaction costs ignored. Dynamic risk metrics that drive rebalancing signals (e.g., volatility targeting) incur turnover. A strategy that looks attractive on a risk-adjusted basis pre-cost can deteriorate significantly once realistic bid-ask spreads and market impact are applied, particularly for less liquid European names like UNA.AS or AFX.DE.

Concluding Thoughts

Dynamic risk management transforms static portfolio statistics into living, breathing signals that reflect actual market conditions. The toolkit built here — rolling volatility, Sharpe, Treynor, beta, Jensen's Alpha, and multi-level VaR — provides the quantitative infrastructure needed to move beyond buy-and-hold intuition toward systematic, evidence-based risk control.

The most productive next step is to integrate these metrics directly into a backtesting loop. Rather than computing risk metrics for analysis alone, wire the rolling Sharpe or volatility estimate into a position-sizing function and observe how a volatility-targeted strategy compares to fixed-size alternatives across the 2015–2025 sample. The difference in drawdown characteristics is often striking.

If you want the full Google Colab notebook with all eight publication-ready charts, RANSAC beta regression, interactive visualizations, and one-click CSV export for all tickers, the complete toolkit is available via AlgoEdge Insights. Every article in the series ships with a production-ready notebook — no setup friction, just open and run.


Most algo trading content gives you theory.
This gives you the code.

3 Python strategies. Fully backtested. Colab notebook included.
Plus a free ebook with 5 more strategies the moment you subscribe.

5,000 quant traders already run these:

Subscribe | AlgoEdge Insights

Top comments (0)