pandas for Stock Data: From Reading a CSV to Moving Averages
If you've collected price data and don't know what to do next, the answer is usually pandas. It's the Python library for working with tabular data, and nearly every calculation in automated trading and backtesting starts there. This article covers the minimum path from a single CSV to moving averages and returns.
⚠️ This article is for educational and informational purposes only. It is not investment advice, and any losses are your own responsibility.
1. Getting the Data into Shape
Price data is usually stored as a CSV like this. Date, open, high, low, close, volume — those six are enough.
date,open,high,low,close,volume
2026-01-02,70100,71200,69800,71000,12345678
2026-01-03,71000,71500,70500,70600,9876543
The thing that matters when reading it with pandas is parsing dates as actual dates. Read them plainly and they become strings, which throws off sorting and every date-range slice you do afterward.
import pandas as pd
df = pd.read_csv(
"samsung.csv",
parse_dates=["date"], # as a date type, not a string
index_col="date", # use the date as the index (row label)
)
df = df.sort_index() # guarantee chronological order — a backtest prerequisite
print(df.head())
print(df.dtypes) # always confirm close is int/float
Make checking dtypes a habit. If a numeric column contains commas (71,000), pandas reads it as a string and every calculation downstream goes quietly wrong.
2. Slicing — Selecting Periods and Columns
# A specific date range (possible because the index is a date)
recent = df.loc["2026-01-01":"2026-03-31"]
# Closing prices only
close = df["close"]
# Conditional filter: days when volume was more than double the average
spike = df[df["volume"] > df["volume"].mean() * 2]
print(f"volume spike days: {len(spike)}")
Give pandas a condition and it builds a table of True/False values; put that inside the brackets and only the True rows survive. This single pattern covers most strategy conditions you'll write.
3. Moving Averages — One Line Each
A moving average is "the mean of the last N closing prices." You use it to reduce the jitter in price and see the trend.
df["ma5"] = df["close"].rolling(window=5).mean()
df["ma20"] = df["close"].rolling(window=20).mean()
print(df[["close", "ma5", "ma20"]].tail())
For the first 19 rows, rolling(20) doesn't have enough data and produces NaN (empty values). That's normal, not an error. But feed that stretch straight into a strategy and you'll get nonsense signals, so trim it.
df = df.dropna() # start from where the indicators are populated
Detecting a golden cross — the short average crossing above the long one — works the same way.
# Below yesterday, above today -> the cross happened today
cross_up = (df["ma5"] > df["ma20"]) & (df["ma5"].shift(1) <= df["ma20"].shift(1))
print(df[cross_up].index) # the dates the crossover occurred
shift(1) means push everything down one row — in other words, "yesterday's value." It's the function you'll reach for most in time-series work, so make sure this one sticks.
4. Computing Returns
# Daily return (change vs. the previous day)
df["ret"] = df["close"].pct_change()
# Cumulative return — 1.0 is your starting capital
df["cum"] = (1 + df["ret"]).cumprod()
print(f"period return: {(df['cum'].iloc[-1] - 1) * 100:.2f}%")
print(f"daily volatility (std): {df['ret'].std() * 100:.2f}%")
Cumulative returns multiply, they don't add. Adding +10% and -10% looks like 0%, but the real result is -1%. Miss that distinction and your backtest comes out better than reality.
Maximum drawdown — how far you fell from a peak — takes two lines.
peak = df["cum"].cummax() # the running high up to that point
dd = df["cum"] / peak - 1 # decline from the peak
print(f"max drawdown (MDD): {dd.min() * 100:.2f}%")
5. Three Mines Beginners Step On
- Not sorting — if the CSV is stored newest-first and you run
rollingon it as-is, you're computing the past from future data.sort_index()is mandatory. - Look-ahead bias — a backtest that buys at today's close on a signal computed from today's close is impossible in reality. Delay signals by a day with
shift(1). - Ignoring gaps — without checking market holidays and missing data, your "20-day moving average" may actually span 30 calendar days.
6. Saving the Result
df.to_csv("samsung_with_indicators.csv", encoding="utf-8-sig")
utf-8-sig keeps non-ASCII characters from turning into garbage when Excel opens the file. Remembering just that makes eyeballing your output much easier.
Summary
parse_dates+index_col+sort_index()— the reading trio- Indicators come from
rolling(), yesterday's value fromshift(1) - Cumulative returns are multiplicative (
cumprod); drawdown is measured againstcummax - Trim the NaN stretch, and apply signals a day later
pandas is vast, but this is roughly 80% of what automated trading actually uses. Look up the rest when you need it.
댓글
댓글 쓰기