Working with price data in pandas
The DateTimeIndex, slicing by date, resampling one timeframe into another, and computing simple vs log returns — the daily grammar of quant work.
pandas is where a trader spends most of their coding life. Ninety percent of the work is not clever maths — it is selecting the right rows, aligning series in time, and transforming prices into returns. Master these few moves and everything downstream gets easier.
The DateTimeIndex is the whole point
A price DataFrame should be indexed by time, not by a plain integer row number. A DateTimeIndex unlocks date-based slicing, resampling, and automatic time-alignment when you combine instruments. If your index is a column of strings, fix that first.
import pandas as pd
import numpy as np
class="c"># Build an illustrative daily series so this runs with no download.
idx = pd.date_range(class="s">"class="n">2024-class="n">01-class="n">01", periods=class="n">250, freq=class="s">"B", tz=class="s">"UTC") class="c"># business days
rng = np.random.default_rng(class="n">42)
class="c"># Geometric random walk: a rough stand-in for a real price path.
returns = rng.normal(class="n">0.0004, class="n">0.012, len(idx))
price = class="n">100 * np.exp(np.cumsum(returns))
df = pd.DataFrame({class="s">"close": price}, index=idx)
print(type(df.index)) class="c"># DatetimeIndex
print(df.head(class="n">3).round(class="n">2))Selecting and slicing by date
With a DateTimeIndex, you select by *time*, in plain language. This is far safer than counting rows, because it survives missing days and reordering.
class="c"># A single day
df.loc[class="s">"class="n">2024-class="n">03-class="n">15"]
class="c"># An inclusive date range (both ends included with .loc)
q1 = df.loc[class="s">"class="n">2024-class="n">01-class="n">01":class="s">"class="n">2024-class="n">03-class="n">31"]
class="c"># Everything from a date onward
recent = df.loc[class="s">"class="n">2024-class="n">06-class="n">01":]
class="c"># Position-based when you truly want class="s">"the last class="n">20 bars"
last20 = df.iloc[-class="n">20:]
print(fclass="s">"Q1 has {len(q1)} bars; last window has {len(last20)} bars")Resampling: change the timeframe
You often have fine bars but want coarser ones — turning 1-minute data into 1-hour, or daily into weekly. Resampling aggregates correctly, and OHLCV has specific aggregation rules: open takes the *first*, high the *max*, low the *min*, close the *last*, and volume the *sum*. Never just average them.
class="c"># Suppose df has open, high, low, close, volume at class="n">1-minute resolution.
ohlcv_rule = {
class="s">"open": class="s">"first",
class="s">"high": class="s">"max",
class="s">"low": class="s">"min",
class="s">"close": class="s">"last",
class="s">"volume": class="s">"sum",
}
hourly = df.resample(class="s">"1h").agg(ohlcv_rule)
class="c"># Resampling can create empty buckets (e.g. no trades overnight).
class="c"># Dropping fully-empty bars keeps the frame honest.
hourly = hourly.dropna(subset=[class="s">"close"])
print(hourly.head())Returns: simple vs log
Raw prices are hard to compare across instruments — a $5 move means something different for a $50 stock than a $5000 one. Returns normalise that. There are two flavours, and knowing when to use each marks the step from beginner to practitioner.
- Simple return
r = P_t / P_(t-1) - 1. Intuitive ("up 2%"), and the right thing to *average across assets* in a portfolio at one point in time. - Log return
l = ln(P_t / P_(t-1)). It is time-additive — the log return over a week is just the sum of daily log returns — and better behaved statistically, which is why most quant maths uses it.
df[class="s">"ret"] = df[class="s">"close"].pct_change() class="c"># simple return
df[class="s">"logret"] = np.log(df[class="s">"close"] / df[class="s">"close"].shift(class="n">1)) class="c"># log return
class="c"># Cumulative growth of $class="n">1 invested — two equivalent routes:
df[class="s">"equity_simple"] = (class="n">1 + df[class="s">"ret"]).cumprod()
df[class="s">"equity_log"] = np.exp(df[class="s">"logret"].cumsum()) class="c"># note: sum, then exp
class="c"># They match (bar tiny floating-point noise):
print(df[[class="s">"equity_simple", class="s">"equity_log"]].tail(class="n">1).round(class="n">6))You can now load, slice, resample, and transform price data fluently. Next we turn prices into signals — computing the classic indicators by hand so you understand exactly what every number means.