σμ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.13·~40 minIntermediate

Resampling

Skill positionModule 2 of 11 · Topic 13 of 20
2.13 Resampling
—Lab score— / 100Total XP—Streak—

Changing the frequency

Data rarely comes at the frequency you need. Ticks arrive by the millisecond, a model wants daily bars, a report wants months. Resampling regroups a time series into new periods. Going from fine to coarse (days to months) is downsampling: each new period holds several old observations, so you must say how to combine them. Going from coarse to fine (weeks to days) is upsampling: there are new periods with no data, so you must say what to put in them.

import pandas as pd

daily = pd.Series(
    [100.0, 101, 103, 102, 104, 105, 103, 106],
    index=pd.bdate_range("2026-02-25", periods=8),    # Wed 25 Feb ... Fri 6 Mar
    name="close",
)

print(daily.resample("ME").last())     # the last price of each month ("ME" = month end)
# 2026-02-28    103.0
# 2026-03-31    106.0
# Freq: ME, Name: close, dtype: float64

print(daily.resample("W-FRI").last())  # the last price of each week, weeks ending on Friday
# 2026-02-27    103.0
# 2026-03-06    106.0
# Freq: W-FRI, Name: close, dtype: float64

Choosing the aggregation

series.resample("ME") alone does nothing: it only describes the periods. The aggregation that follows decides the value, and it must match the meaning of the data. A price is a snapshot, so a month is represented by its last (or first) value. A volume is an amount traded, so a month is the sum. A range uses max and min. Labels follow the rule: "ME" labels a month with its last calendar day, even if that day is a Saturday, and "W-FRI" labels a week with its Friday.

volume = pd.Series([10, 20, 30, 40, 50, 60, 70, 80], index=daily.index)

print(volume.resample("ME").sum().to_dict())
# {Timestamp('2026-02-28 00:00:00'): 60, Timestamp('2026-03-31 00:00:00'): 300}

# Several summaries at once
print(daily.resample("ME").agg(["first", "last", "max"]))
#             first   last    max
# 2026-02-28  100.0  103.0  103.0
# 2026-03-31  102.0  106.0  106.0

Bars from ticks, and upsampling

Charts and models use OHLC bars: the open (first price), high, low and close (last price) of each period. resample("D").ohlc() builds them from intraday prices in one call. Calendar days without any trade, such as weekends, still appear as rows of NaN, so finish with dropna() when you only want days that traded. Upsampling goes the other way: resample("D").ffill() repeats the last known value on the days without data, which is what a price quoted weekly really means. Never use bfill for prices: it copies a future value backwards and leaks tomorrow's information into today.

times = ["2026-03-05 09:30", "2026-03-05 11:00", "2026-03-05 15:59",
         "2026-03-09 09:30", "2026-03-09 12:00", "2026-03-09 15:59"]
ticks = pd.Series([100.0, 101.5, 99.8, 100.7, 102.0, 101.0], index=pd.to_datetime(times))

bars = ticks.resample("D").ohlc()
print(bars)
#              open   high    low  close
# 2026-03-05  100.0  101.5   99.8   99.8
# 2026-03-06    NaN    NaN    NaN    NaN
# 2026-03-07    NaN    NaN    NaN    NaN
# 2026-03-08    NaN    NaN    NaN    NaN
# 2026-03-09  100.7  102.0  100.7  101.0

print(bars.dropna().index.strftime("%d %b").tolist())   # ['05 Mar', '09 Mar']   only the days that traded
# Upsampling: observations on the 2nd, 6th and 10th, spread over every day with ffill
weekly = pd.Series([100.0, 103.0, 101.0], index=pd.to_datetime(["2026-03-02", "2026-03-06", "2026-03-10"]))

print(weekly.resample("D").ffill().tolist())
# [100.0, 100.0, 100.0, 100.0, 103.0, 103.0, 103.0, 103.0, 101.0]

print(int(weekly.resample("D").mean().isna().sum()))   # 6   without a fill rule the new days are empty
Resampling can leak the futureA monthly bar is only known at the end of the month. If you attach the month-end close to the first day of that month, a strategy appears to know the future. Keep the label at the end of the period, as "ME" does, use ffill (never bfill) for upsampling, and shift a monthly signal forward before using it on daily data.
  • series.resample(rule) describes the periods ("D", "W-FRI", "ME", "h"); an aggregation after it computes the values.
  • Prices: last or ohlc(); volumes: sum; empty periods show up as NaN, so dropna() removes them.
  • Upsampling needs a fill rule: ffill() repeats the last known value; never bfill() for prices, it leaks the future.

Next up: rolling windows, statistics such as moving averages computed over a sliding window of recent periods.

Lab · what's graded

  • Take the last price of each month with month_end_close25 pts
  • Total the volume of each week with weekly_volume25 pts
  • Build daily bars from intraday prices with daily_ohlc25 pts
  • Spread irregular observations over every day with fill_to_daily25 pts
Explorer · lab-46-1
▾ lab-46-1
…
bash · in-browser
learner@quantcraft:~/lab-46-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-46-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