pandas for Algo Trading Data: A Practical Crash Course for Candle Data

pandas for Algo Trading Data: A Practical Crash Course for Candle Data

11 Aug 2026 7 mins read

Pandas becomes useful for trading research when you make time, price fields and table grain explicit. Sort each instrument’s timestamps, define the session and resampling bucket, aggregate OHLCV fields deliberately, keep missing observations visible, shift features before they become decision inputs and validate joins. These steps make transformations reproducible; they do not prove that the underlying data or strategy is sound.

Start with a table that knows what one row means

Before writing a pandas expression, state what one row represents. For a candle table, the minimum contract is:

FieldMeaning
symbol or instrument_keyWhich security the row belongs to
timestampWhen the observation occurred, and whether it marks bar start or bar end
open, high, low, closeThe four prices for that interval
volumeSource-reported quantity, with units documented

If several securities share a timestamp, the grain is still one row per (symbol, timestamp), not one row per timestamp. Sort within that grain before applying a time-series operation:

DF = (

DF.ASSIGN(TIMESTAMP=PD.TO_DATETIME(DF[“TIMESTAMP”]))

.SORT_VALUES([“SYMBOL”, “TIMESTAMP”])

)

Do not assume that a symbol is a permanent identity. Retain the source identifier and state the timestamp convention. A 09:20 label may mean a bar beginning at 09:20 or a bar that finished at 09:20; those are different inputs to a signal.

Set the Indian-market clock before resampling

Pandas does not know what an NSE session means. NSE lists the normal/odd-lot cash-market window as 09:15 to 15:30, while its auction and post-close pages describe separate market states. A candle tutorial should therefore use a named regular-continuous window and keep other states separate. (1), (2)

The full market-state explanation belongs in Market Mechanics for Code-First Traders, and source/session ingestion belongs in Building a Clean NSE Data Pipeline.

For a naive timestamp, tz_localize("Asia/Kolkata") attaches a timezone without moving the displayed clock time. For an aware timestamp, tz_convert("Asia/Kolkata") changes the timezone of the same instant. (3)

LOCAL = PD.TO_DATETIME(DF[“TIMESTAMP”]).DT.TZ_LOCALIZE(“ASIA/KOLKATA”)

CONVERTED = AWARE_TIMESTAMP.DT.TZ_CONVERT(“ASIA/KOLKATA”)

If the source timezone is unknown, keep the rows out of the canonical table until the convention is resolved. A timezone conversion does not prove that the value was available at the time a strategy would have acted.

Resample OHLCV with named bucket rules

Resampling changes the observation interval. For each bucket (B_t), the usual OHLCV mapping is:

OPEN = FIRST OPEN IN B_T

HIGH = HIGHEST HIGH IN B_T

LOW = LOWEST LOW IN B_T

CLOSE = LAST CLOSE IN B_T

VOLUME = SUM OF VOLUME IN B_T

This is a local calculation, not an exchange-published candle. Pandas’ resample API requires a datetime-like index or datetime-like field and exposes origin, offset, closed and label; record those choices instead of inheriting a hidden default. (4)

OHLCV_MAP = {

“OPEN”: “FIRST”,

“HIGH”: “MAX”,

“LOW”: “MIN”,

“CLOSE”: “LAST”,

“VOLUME”: “SUM”,

}

DF_30M = (

DF.SET_INDEX(“TIMESTAMP”)

.GROUPBY(“SYMBOL”)

.RESAMPLE(

“30MIN”,

ORIGIN=”START_DAY”,

OFFSET=”15MIN”,

CLOSED=”LEFT”,

LABEL=”LEFT”,

)

.AGG(OHLCV_MAP)

.DROPNA(SUBSET=[“OPEN”])

.RESET_INDEX()

)

The 15min offset aligns the first bucket to 09:15 rather than a midnight-derived 09:00 boundary. That is correct only for this stated session convention. Record the input and output frequency, timezone, session inclusion rule and treatment of partial or empty buckets.

Worked synthetic example: the bucket rule changes the candle

The following values are synthetic and are not NSE observations:

5-minute timeOpenHighLowCloseVolume
09:15100.0101.099.0100.5100
09:20101.0102.0100.0101.0110
09:2599.0100.098.599.590
09:30100.0102.099.0101.5120
09:35101.0103.0100.0102.5130
09:40102.0104.0101.0103.5140
09:45103.0104.0102.0102.5150
09:50102.0103.0101.0102.2160

With buckets aligned to 09:15, the output is:

30-minute bucketOpenHighLowCloseVolume
09:15–09:45100.0104.098.5103.5690
09:45–10:15103.0104.0101.0102.2310

Default 30-minute bins beginning at 09:00 produce different OHLC values and volumes of 300 and 700 because the rows fall into different buckets. Neither result is a pandas error. The bucket convention is part of the data definition and should be recorded with the output.

Calculate returns without confusing fractions and percentages

For a close (C_t), the simple return is:

R_T = (C_T / C_(T-1)) – 1

pct_change returns the fractional value. Multiply by 100 only when displaying a percentage, and leave the first observation as NaN because it has no prior close. Current pandas documentation requires fill_method=None; do not rely on implicit filling of missing prices. (5)

DF[“RETURN_1”] = (

DF.GROUPBY(“SYMBOL”)[“CLOSE”]

.PCT_CHANGE(FILL_METHOD=NONE)

)

For closes of 100, 101, 99 and 102:

CloseFractional returnPercentage display
100NaNNaN
1010.0100001.0000%
99−0.019802−1.9802%
1020.0303033.0303%

Record the return type, holding interval and price basis (raw, vendor_adjusted or locally_adjusted). A return column is not a win-rate or profit-factor metric, and it does not settle whether the source series is complete. The broader adjustment and point-in-time audit belongs in Topic 8.

Build rolling features and align their availability

A 20-row window on 5-minute bars is not a 20-session moving average. Integer windows count observations; elapsed-time windows use timestamps. Missing bars can change what a row-count window means, so record the window type, size and min_periods. Pandas also exposes endpoint and label choices for rolling windows. (6)

DF[“CLOSE_MA_20”] = (

DF.GROUPBY(“SYMBOL”)[“CLOSE”]

.ROLLING(WINDOW=20, MIN_PERIODS=20)

.MEAN()

.RESET_INDEX(LEVEL=0, DROP=TRUE)

)

DF[“CLOSE_MA_20_AVAILABLE”] = (

DF.GROUPBY(“SYMBOL”)[“CLOSE_MA_20”]

.SHIFT(1)

)

For closes 100, 101, 99 and 102, a three-row mean with min_periods=3 is NaN, NaN, 100.000000, 100.666667. After shifting by one row it is NaN, NaN, NaN, 100.000000. This demonstrates alignment, not a trading signal. The shift(1) operation moves values across ordered rows; supplying a freq changes index labels instead, which is a different operation. (7)

Full strategy-level look-ahead and survivorship examples belong in Why Backtests Lie: Look-Ahead and Survivorship Bias.

Join related tables without multiplying rows

If prices and moving averages are each intended to have one row per (symbol, timestamp), declare that relationship:

JOINED = PRICES.MERGE(

MOVING_AVERAGE,

ON=[“SYMBOL”, “TIMESTAMP”],

HOW=”LEFT”,

VALIDATE=”ONE_TO_ONE”,

INDICATOR=TRUE,

)

Pandas’ validate checks relationships such as one_to_one, while indicator records left_only, right_only and both. Duplicate keys can otherwise multiply rows; an inner join can silently drop observations. Check row counts before and after the merge, and inspect null keys because pandas’ null-key matching differs from ordinary SQL joins. (8)

If the right table contains two rows for one key, let the merge fail. Do not take the last row until you know whether the duplicate is a correction, session collision, symbol mismatch or parser defect. Preserve the original records and send the source investigation to Topic 8.

Run local transformation checks before moving on

These assertions check the transformation, not the quality of the source:

QA = DF.SORT_VALUES([“SYMBOL”, “TIMESTAMP”]).COPY()

ASSERT QA.GROUPBY(“SYMBOL”)[“TIMESTAMP”].APPLY(

LAMBDA S: S.IS_MONOTONIC_INCREASING

).ALL()

ASSERT NOT QA.DUPLICATED([“SYMBOL”, “TIMESTAMP”]).ANY()

ASSERT (QA[“HIGH”] >= QA[[“OPEN”, “CLOSE”, “LOW”]].MAX(AXIS=1)).ALL()

ASSERT (QA[“LOW”] <= QA[[“OPEN”, “CLOSE”, “HIGH”]].MIN(AXIS=1)).ALL()

ASSERT (QA[“HIGH”] >= QA[“LOW”]).ALL()

ASSERT (QA[“VOLUME”] >= 0).ALL()

Use isna() or notna() to expose missing fields; pandas documents that missing values can have different sentinel representations. (9) Do not forward-fill every gap: a missing 5-minute bar may be a source failure, a suspension, a no-trade interval or an excluded auction state. Data QA: Why Your Dataset Lies owns that source, calendar and point-in-time audit.

What this tutorial does not prove

Pandas cannot tell you whether a provider retained securities that later disappeared, whether current constituents were applied to old dates, whether an adjustment method matches the research question, whether timestamps describe publication time or observation time, whether a source licence permits redistribution, or whether orders and fills are realistic. A notebook can run without errors while any of those conditions is false.

Once the table passes the data audit, Backtesting 101: Vectorized vs Event-Driven explains the engine choice, while Build Your First Backtester in pandas extends a clean transformation into signal and trade records.

Tested environment and limitation

The synthetic examples and checks were executed on Linux x86_64 with Python 3.12.13, pandas 3.0.5 and NumPy 2.3.5. The deterministic fixture matched its recorded canonical hash. This is not a claim that the commands were executed on Windows. Use Python Setup for Trading (Windows) for setup, then rerun the fixture there before treating platform-specific behaviour as resolved.

The examples demonstrate pandas mechanics, not NSE market behaviour, data-vendor coverage or strategy performance.

Sources

(1) NSE India, “Market Timings”: https://www.nseindia.com/static/market-data/market-timings

(2) NSE India, “Closing Auction Session”: https://www.nseindia.com/static/products-services/closing-auction-session

(3) pandas, “Time series / date functionality”: https://pandas.pydata.org/docs/user_guide/timeseries.html

(4) pandas, “DataFrame.resample”: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.resample.html

(5) pandas, “Series.pct_change”: https://pandas.pydata.org/docs/reference/api/pandas.Series.pct_change.html

(6) pandas, “DataFrame.rolling”: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.rolling.html

(7) pandas, “DataFrame.shift”: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.shift.html

(8) pandas, “DataFrame.merge” and “Merge, join, concatenate and compare”: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.merge.html; https://pandas.pydata.org/docs/user_guide/merging.html

(9) pandas, “Working with missing data”: https://pandas.pydata.org/docs/user_guide/missing_data.html

About the author

Pranay

Senior Researcher and Editor

Pranay is the co-founder of DailyBulls.in, a trader-focused market research and learning platform, and OIHelper.com, a platform focused on open interest analysis. He has 5+ years of experience following Indian markets, with core interests in technical analysis, stock screeners, open interest analysis, and structured research workflows.He is also a coder and spends much of his time building custom stock screeners, research tools, and AI-assisted workflows that help organize market data, improve research efficiency, and make technical learning more practical for traders and market learners. Through DailyBulls.in, he shares educational content, research-driven articles, and workflow ideas built around technical analysis, market behavior, and data-backed learning.His work has also been referenced in academic publishing, including an MDPI-published paper in the Journal of Risk and Financial Management.

Share this insight

Spread the Alpha

If this analysis helped you, pass it along to your trading desk or community.

Leave a Comment