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 rolling on 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 from shift(1)
  • Cumulative returns are multiplicative (cumprod); drawdown is measured against cummax
  • 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.

댓글

이 블로그의 인기 게시물

한국투자증권 KIS API로 실시간 시세 받기 (WebSocket 실전)

파이썬으로 업비트 API 연동하기 — 시세 조회부터 주문까지 기초

Go로 자동매매 신호봇 프레임워크 설계하기