The anatomy of a bar
Worked examples
Read the claim, then run it and check the machine agrees. One at a time — nothing here is taken on trust.
import numpy as np
import pandas as pd
import qadata
close = qadata.trending_prices() # 756 seeded daily closes
n = len(close)
rng = np.random.default_rng(7) # fixed seed: same bars forever
open_ = close.shift(1).fillna(close.iloc[0] / 1.0004) * (1 + 0.0015 * rng.standard_normal(n))
wick_hi = np.abs(0.004 * rng.standard_normal(n))
wick_lo = np.abs(0.004 * rng.standard_normal(n))
bars = pd.DataFrame({
"open": open_,
"high": np.maximum(open_, close) * (1 + wick_hi),
"low": np.minimum(open_, close) * (1 - wick_lo),
"close": close,
"volume": np.round(1e6 * np.exp(0.3 * rng.standard_normal(n))).astype(int),
})
print(bars.head(3).round(4))
ok_hi = (bars.high >= bars[["open", "close"]].max(axis=1) - 1e-12).all()
ok_lo = (bars.low <= bars[["open", "close"]].min(axis=1) + 1e-12).all()
print("high is the envelope top:", bool(ok_hi), "| low is the bottom:", bool(ok_lo))
print(f"mean intraday range: {((bars.high - bars.low) / bars.close).mean():.4%}")
print(f"mean close-to-close : {close.pct_change().abs().mean():.4%}")
A bar is born: opens gap off the prior close, wicks extend the envelope beyond both open and close, and the high/low invariants hold on all 756 rows by construction. The last two lines are the slide's fact: the day's range averages 1.47% while the close only moves 0.81% — half the action never touches the column your backtests read.
import numpy as np
import qadata
adj = qadata.trending_prices() # the truth, split-adjusted
raw = adj.copy()
raw[raw.index < "2021-12-02"] *= 2.0 # vendor's unadjusted file: 2:1 split
raw_rets, adj_rets = raw.pct_change(), adj.pct_change()
d = "2021-12-02"
print(f"split day 'return' raw: {raw_rets[d]:+.4%} truth: {adj_rets[d]:+.4%}")
print(f"worst day raw: {raw_rets.min():+.4%} truth: {adj_rets.min():+.4%}")
print(f"ann vol raw: {raw_rets.std() * np.sqrt(252):.4%} truth: {adj_rets.std() * np.sqrt(252):.4%}")
One line of bookkeeping manufactures the crash: −50.51% on a day when holders lost 1.02%. Note the blast radius — the worst-day statistic is off by a factor of 16, and annualized volatility doubles from 16.14% to 33.37% because one squared "return" of 0.255 swamps three years of honest 0.0001s. Every risk number in modules 5-7 dies if this row survives.
close_before = 60.00 # cum-dividend close
dividend = 1.20 # cash paid per share on the ex-date
close_exdiv = 58.80 # stock opens lower by the payout, nothing else happens
raw_ret = close_exdiv / close_before - 1
total_ret = (close_exdiv + dividend) / close_before - 1
factor = 1 - dividend / close_before # back-adjustment factor
adj_before = close_before * factor
adj_ret = close_exdiv / adj_before - 1
print(f"raw price return : {raw_ret:+.4%} (looks like a loss)")
print(f"holder's truth : {total_ret:+.4%} (price drop + cash in hand)")
print(f"factor {factor:.4f} -> adjusted prior close {adj_before:.2f} -> return {adj_ret:+.4%}")
print(f"2% yield ignored for 40 years compounds to: {0.98 ** 40 - 1:+.2%}")
A 2% dividend prints as a −2% "return" in price-only data, and the multiplicative factor 1 − div/price = 0.98 on all earlier closes erases exactly that phantom loss. Small lie, huge compound interest: ignore a 2% annual payout for 40 years and your "index" is off by −55.43%. This is the entire difference between a price index and a total-return index.
Check the concept
One question at a time. Unsure? Revisit the lecture, then answer.
The challenge
Pass the quiz to unlock the challenge — your code will still be waiting here.