AlgoPro UniversityCourse home
Part 3 · Lesson

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.

pythonA tidy, time-indexed price frame
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.

pythonLabel-based, time-aware selection
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.

pythonResample OHLCV to a higher timeframe
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.

pythonCompute both, and cumulative growth
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))
Price and the growth of $1 rebuilt from returnsJanDec
The price path (blue) and the growth of $1 reconstructed from its returns (violet) trace the same journey — a visual proof that your return calculation is self-consistent.

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.