σμ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.17·~45 minIntermediate

Corporate actions: splits and dividends

Skill positionModule 2 of 11 · Topic 17 of 20
2.17 Corporate actions: splits and dividends
—Lab score— / 100Total XP—Streak—

A price history that lies

When a company splits its shares 4-for-1, every holder gets four shares for each one they had, and the price falls to a quarter. Nobody lost anything, but the raw history shows a -75% day. A cash dividend does the same on a smaller scale: on the ex-dividend date the price drops by about the dividend. A strategy that reads raw prices will see crashes that never hurt anyone. The fix is to adjust the history: rescale all the days before the event so the series is continuous and comparable with today's prices.

import pandas as pd

index = pd.bdate_range("2026-03-02", periods=6)
raw = pd.Series([480.0, 484.0, 488.0, 123.0, 124.5, 126.0], index=index, name="close")   # 4-for-1 split on 5 March

print(raw.pct_change().round(4).tolist())
# [nan, 0.0083, 0.0083, -0.748, 0.0122, 0.012]    the -74.8% day never happened to anyone

Adjusting for a split

Only the days before the split change: divide their prices by the ratio (4 for a 4-for-1 split). Today's prices stay as quoted, so the adjusted series ends at the real current price. Build a True/False mask with index >= split_date (a string works against a DatetimeIndex) and use where, which keeps a value where the mask is True and takes the replacement elsewhere. Volume goes the other way: four times as many shares trade after the split, so earlier volumes are multiplied by the ratio. A reverse split uses a ratio below 1 and the same formulas.

split_date = "2026-03-05"
adjusted = raw.where(raw.index >= split_date, raw / 4)    # the days before the split, divided by 4

print(adjusted.tolist())                      # [120.0, 121.0, 122.0, 123.0, 124.5, 126.0]
print(adjusted.pct_change().round(4).tolist())   # [nan, 0.0083, 0.0083, 0.0082, 0.0122, 0.012]   no more crash
# Volume goes the other way: older days are multiplied
volume = pd.Series([1000, 1100, 900, 4200, 4000, 4300], index=index)
print(volume.where(volume.index >= split_date, volume * 4).tolist())   # [4000, 4400, 3600, 4200, 4000, 4300]

Adjusting for a dividend

A dividend is smaller than a split, so the adjustment is a factor close to 1: 1 - dividend / previous close, where the previous close is the last close before the ex-dividend date. Multiply every price before the ex-date by that factor. The close just before the ex-date then drops by exactly the dividend, the ex-day move shows only what the market really did, and returns computed from the adjusted series include the dividend. This adjusted close is what return calculations should use. Put the factor in its own small function so that you can test it and reuse it.

def dividend_factor(prev_close, dividend):
    return 1 - dividend / prev_close


prices = pd.Series([100.0, 101.0, 102.0, 100.8, 101.5, 102.5], index=index)   # a 1.50 dividend, ex-date 5 March
ex_date, dividend = "2026-03-05", 1.5

prev_close = prices[prices.index < ex_date].iloc[-1]       # 102.0, the last close before the ex-date
factor = dividend_factor(prev_close, dividend)
print(round(factor, 6))                                    # 0.985294

adjusted = prices.where(prices.index >= ex_date, prices * factor)
print(adjusted.round(4).tolist())                          # [98.5294, 99.5147, 100.5, 100.8, 101.5, 102.5]
print(prices.pct_change().round(4).tolist()[3], adjusted.pct_change().round(4).tolist()[3])   # -0.0118 0.003
Adjusted for returns, raw for tradingAdjusted prices are for measuring returns and comparing across time. They are not what you pay: an order is placed at the raw price quoted that day, and an old adjusted price may be below the real price at which the stock traded. Keep both columns (close and adj_close) and say which one a calculation uses.
  • A split divides the old prices by the ratio and multiplies the old volumes by it; only the days before the split change.
  • A dividend multiplies the prices before the ex-date by 1 - dividend / previous close.
  • Use where(index >= date, adjusted) with a mask, not a loop; compute returns from adjusted prices, trade at raw ones.

Next up: line plots with matplotlib, drawing the clean price series you now have.

Lab · what's graded

  • Make the prices comparable across a split with adjust_for_split25 pts
  • Scale the old volumes with adjust_volume_for_split25 pts
  • Compute the dividend adjustment factor with dividend_factor25 pts
  • Remove the dividend drop from the history with adjust_for_dividend25 pts
Explorer · lab-50-1
▾ lab-50-1
…
bash · in-browser
learner@quantcraft:~/lab-50-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

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