σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademySkills
Ctrl K
AO
+1.2k Learners
LoginJoin ↗
Skills
AO
+1.2k Learners
Login
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Python skill›Module 2 · NumPy & pandas for market data›Topic 2.14·~40 minIntermediate

Rolling windows

Skill positionModule 2 of 11 · Topic 14 of 20
2.14 Rolling windows
—Lab score— / 100Total XP—Streak—

A window that slides

Today's price means little on its own; traders compare it with the recent past. A rolling window keeps the last n observations and moves forward one step at a time, so each date gets a statistic computed from its own recent history: a moving average, a recent volatility, a recent high. In pandas, series.rolling(n) describes the window and a statistic such as .mean() computes it on every position at once.

import pandas as pd

prices = pd.Series(
    [100.0, 102, 101, 105, 107, 106, 110],
    index=pd.bdate_range("2026-03-02", periods=7),
    name="close",
)

moving_average = prices.rolling(3).mean()
print(moving_average.round(2).tolist())
# [nan, nan, 101.0, 102.67, 104.33, 106.0, 107.67]

print(prices.rolling(3).max().tolist())   # [nan, nan, 102.0, 105.0, 107.0, 107.0, 110.0]   the 3-day high

The start of the series

The first n - 1 positions do not have a full window yet, so the result is NaN there. That is honest: an average of two prices is not a 3-day average. If you really want a value earlier, min_periods=1 computes with what is available, and expanding() uses everything since the start instead of a fixed window. Statistics that need several points, such as the standard deviation, are NaN until enough observations have arrived. A series that already begins with NaN, like simple returns, pushes the first valid window one step later.

# min_periods=1: use whatever is available at the start
print(prices.rolling(3, min_periods=1).mean().round(2).tolist())   # [100.0, 101.0, 101.0, 102.67, 104.33, 106.0, 107.67]

# expanding(): everything since the first day, not a fixed window
print(prices.expanding().mean().round(2).tolist())   # [100.0, 101.0, 101.0, 102.0, 103.0, 103.5, 104.43]

# Rolling volatility of the returns: the first return is NaN, so three returns exist only from the 4th day
returns = prices.pct_change()
print(returns.rolling(3).std().round(4).tolist())   # [nan, nan, nan, 0.0249, 0.0248, 0.0246, 0.0237]

From windows to signals

Rolling statistics become trading signals when you compare them. A z-score says how far today's price is from its own recent mean, in units of its recent standard deviation: (price - rolling mean) / rolling std. Build the rolling object once (r = prices.rolling(3)) and use it twice. A moving-average crossover compares a fast average with a slow one: while the fast one is above, the recent trend is up. A comparison gives booleans, and astype("int64") turns them into a 0/1 signal. Comparisons with NaN are False, so the signal is 0 until both averages exist.

# z-score of each price against its own 3-day window
rolling = prices.rolling(3)
zscore = (prices - rolling.mean()) / rolling.std()
print(zscore.round(2).tolist())   # [nan, nan, 0.0, 1.12, 0.87, 0.0, 1.12]
# Moving-average crossover: 1 while the 2-day average is above the 4-day average
fast = prices.rolling(2).mean()
slow = prices.rolling(4).mean()
signal = (fast > slow).astype("int64")
print(signal.tolist())   # [0, 0, 0, 1, 1, 1, 1]

# Trade on the next day: the signal of today's close cannot be traded before tomorrow
print(signal.shift(1).fillna(0).astype("int64").tolist())   # [0, 0, 0, 0, 1, 1, 1]
Do not look into the futureA rolling window ends on the current row, so it only uses the past. center=True puts the current row in the middle of the window, using future prices: it looks great on a chart and cannot be traded. A signal computed from today's close should be applied tomorrow, hence shift(1).
  • series.rolling(n).mean() (or std(), max()...) computes a statistic over the last n observations at every position.
  • The first n - 1 entries are NaN; min_periods and expanding() change that on purpose.
  • Z-scores and moving-average crossovers turn windows into signals; shift a signal before trading it, and avoid center=True.

Next up: handling missing data, what to do with the NaN values that real price tables always contain.

Lab · what's graded

  • Smooth the prices with moving_average25 pts
  • Measure recent risk with rolling_volatility25 pts
  • Standardise each price against its window with rolling_zscore25 pts
  • Turn two averages into a signal with ma_crossover25 pts
Explorer · lab-47-1
▾ lab-47-1
…
bash · in-browser
learner@quantcraft:~/lab-47-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-47-1 goes back to the starter files and the terminal is cleared. Your current solution will be lost — this can't be undone.

Will be reset
  • main.py → starter code
  • Terminal history and test output
  • Unsubmitted changes
Will be kept
  • Best lab score
  • Your XP and streak
  • Lecture progress and notes

Log in to run your code.

Every lecture is free to read. Create an account or log in to run code, run tests and submit this lab.

  • In-browser IDE with a bash terminal — nothing to install
  • Instant grading, XP and your place on the skill
Create an accountLog in
OR
Continue with GitHub