← 返回教程库

用 pandas 处理行情数据:量化的第一项基本功

最后更新 2026-06-21
L5 · 量化投资 📖 L5-22 Python 量化工具链 ⏱ 约 15 分钟 R2 · 注意风险
你将学到
  • 会用 pandas 读取与清洗行情数据(日期索引、缺失值、排序)
  • 会算日收益率、累计收益、移动均线等基础指标
  • 建立「数据干净是量化第一步」的意识

很多人一上来学量化,第一件事就是打开教程找「策略」:双均线金叉买、死叉卖,回测一跑,曲线漂亮,激动得不行。可真到自己写,往往第一步就崩了——拿到一份行情数据,日期是乱的、中间缺了几天、有一行价格莫名其妙是 0,算出来的收益率全是 NaNinf。策略逻辑没错,错的是喂给它的数据。

量化圈有句老话:垃圾进,垃圾出(garbage in, garbage out)。 你的策略再精妙,喂进去的是脏数据,出来的就是脏结果。所以量化真正的第一项基本功,不是写策略,是把行情数据洗干净、算明白。这篇就用 pandas 把这套流程从头跑一遍。

⚠️ 风险提示

本文是数据处理教学,所有数据都是用程序生成的模拟序列,不是任何真实标的的真实行情。文中算的收益率、均线只为演示 pandas 用法,不构成任何买卖信号、不预测涨跌、不导向实盘操作。投资有风险,详见 /disclaimer/

先说清楚 pandas 到底帮你干什么

pandas 是 Python 里处理表格数据的标准工具。你可以把它想成「能写代码操控的 Excel」:行情数据本质就是一张表,每一行是某一天,每一列是开盘价、收盘价、成交量这些字段。Excel 拖鼠标做的事——排序、筛选、算新列、求平均——pandas 用一两行代码就能做,而且能处理几十万行不卡。

行情数据有个最常见的格式叫 OHLCV:开盘价(Open)、最高价(High)、最低价(Low)、收盘价(Close)、成交量(Volume)。后面我们要造的、要洗的、要算的,都围绕这张 OHLCV 表。

环境准备很简单,命令行里装两个库就行:

pip install pandas numpy

第一步:造一份模拟行情(为了可复现)

教学最怕的是「我这跑得通,你那跑不通」。如果让你去下载真实行情,每个人拿到的数据不一样、还可能有版权和接口问题。所以这里用 numpy 生成一段模拟价格序列——固定随机种子,保证你照抄代码跑出来的数字和我的一模一样。

import numpy as np
import pandas as pd

# 固定随机种子:保证每次运行、每个人跑出来的随机数都一样,结果可复现
np.random.seed(42)

# 造 100 个交易日的日期。freq='B' 表示 business day(只取工作日,跳过周末)
dates = pd.date_range(start='2024-01-01', periods=100, freq='B')

# 用「随机游走」模拟收盘价:每天的涨跌幅是一个均值 0、标准差 1% 的随机数
# 真实股价的走势近似随机游走,这样造出来的曲线比纯随机数像样得多
daily_returns = np.random.normal(loc=0.0, scale=0.01, size=100)
close = 100 * (1 + daily_returns).cumprod()   # 从 100 元起步,逐日累乘涨跌幅

# 用收盘价反推出一份完整的 OHLCV 表(纯演示,不追求严谨)
df = pd.DataFrame({
    'date': dates,
    'open': close * (1 + np.random.normal(0, 0.003, 100)),   # 开盘价在收盘价附近抖动
    'high': close * (1 + np.abs(np.random.normal(0, 0.005, 100))),  # 最高价略高于收盘
    'low':  close * (1 - np.abs(np.random.normal(0, 0.005, 100))),  # 最低价略低于收盘
    'close': close,
    'volume': np.random.randint(1_000_000, 5_000_000, 100),  # 成交量随机整数
})

print(df.head())   # 看前 5 行长什么样

这段在干嘛:先用 cumprod(累乘)把每天的涨跌幅滚成一条价格曲线,再围绕收盘价造出开高低和成交量,最后塞进一个 DataFrame(pandas 的表格对象)。df.head() 打印前 5 行,是你拿到任何数据后该做的第一个动作——先看一眼它长什么样,别急着算。

跑出来大概是这样(截取):

        date        open       high        low       close   volume
0 2024-01-01   99.86...   100.71...    99.43...   100.49...  3267441
1 2024-01-02  100.30...   100.55...    99.81...   100.35...  1411319
...

第二步:把日期变成索引,并排好序

原始数据里,日期只是普通一列。但行情分析几乎所有操作都按时间来——取某段时间、按日对齐、算移动窗口——所以第一件事是把日期设成索引(index),相当于给这张表换了个「按时间查」的目录。

# 把 date 列设为索引。inplace=True 表示直接改原表,不返回新表
df.set_index('date', inplace=True)

# 按时间从早到晚排序。真实数据里日期顺序经常是乱的,必须先排好
df.sort_index(inplace=True)

print(df.index)   # 确认索引现在是 DatetimeIndex(pandas 专门的时间索引类型)

为什么排序这么重要?因为后面算收益率、算均线,都默认「上一行就是前一个交易日」。如果日期是乱的,你算的「今天比昨天」其实是「今天比某个随机的别的日子」,结果全错,而且程序不会报错——它照算不误,只是算的是垃圾。这是新手最容易踩、又最难发现的坑。

🚧 避坑

坑:拿到数据不排序就直接算。 很多数据源(尤其是从接口拿的、多文件拼起来的)日期顺序是乱的或倒序的。你不 sort_index 就算 pct_change,程序照跑不报错,但每一个收益率都是错的。为什么坑:错误是「静默」的,曲线照样画得出来,你根本看不出哪里不对,等拿去做决策才发现全盘皆错。怎么避:养成习惯,设完时间索引立刻 sort_index(),把它当成肌肉记忆。

第三步:处理缺失值

真实行情里缺数据是常态:停牌的日子没价格、接口偶尔吐回一个空值、不同来源拼表时对不齐。pandas 用 NaN(Not a Number)表示缺失。我们先手动制造几个缺失值,再演示怎么查、怎么补。

# 故意把第 10、20、30 行的 close 改成缺失值,模拟真实数据的窟窿
df.loc[df.index[[10, 20, 30]], 'close'] = np.nan

# 第一步永远是「先看有多少缺失」,别上来就补
print(df.isna().sum())   # 每一列各有多少个 NaN

# 对行情这类时间序列,常用「前向填充」:用前一个交易日的值补上
# 逻辑是:停牌期间价格视为不变,沿用上一个有效价格
df['close'] = df['close'].ffill()

print(df['close'].isna().sum())   # 再查一次,应该是 0 了

这段的关键不是「怎么补」,而是先搞清楚缺了多少、为什么缺,再决定怎么处理。这里用 ffill(前向填充)是因为停牌期间「价格视为不变」在金融上说得通。但请注意,填充方式必须想清楚业务含义,不是随手填个 0 或填个平均值就完事。

🚧 避坑

坑:缺失值直接 dropna 一删了之,或者随手填 0。 直接删行,会让时间序列出现「断点」——原本连续的交易日中间被抽掉几天,你后面算的「日收益率」就跨过了那段空洞,变成了「隔好几天的收益率」,数值被严重高估。填 0 更糟,价格突然从 100 跳到 0 再跳回来,会算出 -100% 和 +∞ 这种荒谬的收益率。为什么坑:这两种做法都会在你看不见的地方污染整条序列。怎么避:先 isna().sum() 搞清缺失规模和位置,再按业务含义选填充方式(行情常用 ffill),删除是最后才考虑的手段。

第四步:算日收益率和累计收益

数据干净了,终于可以算指标。最基础的就是日收益率——今天相对昨天涨跌了百分之几。pandas 有现成的 pct_change(),一行搞定。

# pct_change():自动用「(今天 - 昨天) / 昨天」算每日涨跌幅
df['daily_return'] = df['close'].pct_change()

# 第一行没有「昨天」,所以是 NaN,这是正常的,去掉它再继续
df['daily_return'] = df['daily_return'].fillna(0)

# 累计收益:把每天的涨跌幅连乘起来,看从头到现在总共涨跌了多少
# (1 + 每日收益) 连乘 - 1,得到累计收益率
df['cum_return'] = (1 + df['daily_return']).cumprod() - 1

print(df[['close', 'daily_return', 'cum_return']].tail())   # 看最后 5 行

这段在干嘛:pct_change 把价格序列转成涨跌幅序列;cumprod(累乘)再把每天的涨跌幅滚成「从第一天到今天总共赚/亏了多少」。注意这里绝对不能用累加——涨 10% 再跌 10% 不是回到原点,是亏了 1%,必须用 (1+r) 连乘才对。这是金融数据处理里一个真实的、新手常错的细节。

🚧 避坑

坑:用「未来的数据」算「过去的信号」(未来函数)。 这是量化里最致命、最隐蔽的错误。比如你想做一个「今天的信号决定明天买不明天买」的研究,正确做法是用 df['signal'].shift(1) 把信号往后挪一天,表示「拿昨天收盘后才知道的信号,指导今天的动作」。如果你不 shift,直接拿今天的收盘价算出今天的信号、又用它「决定」今天的买卖,相当于偷看了今天的答案再下注。为什么坑回测曲线会美得不真实,一拿到真实环境立刻原形毕露,因为现实里你不可能在收盘前就知道收盘价。怎么避:凡是「信号 → 动作」之间,老老实实 shift(1),并反复问自己「这个数,在我做决定的那一刻真的已经知道了吗?」

第五步:算移动均线

移动均线(Moving Average)就是「最近 N 天收盘价的平均」,是行情分析里最常见的派生指标。pandas 用 rolling(滚动窗口)一行算出来。

# rolling(window=5):开一个 5 天的滑动窗口;.mean() 算窗口内平均
df['ma5'] = df['close'].rolling(window=5).mean()    # 5 日均线
df['ma20'] = df['close'].rolling(window=20).mean()  # 20 日均线

# 前几天因为凑不够窗口长度,均线会是 NaN,这是正常的
print(df[['close', 'ma5', 'ma20']].head(25))

这段在干嘛:rolling(window=5) 像一个在数据上从左往右滑动的「取景框」,每滑一格就框住最近 5 天,.mean() 算出框内平均。20 日均线同理,只是框更大。注意头部那些 NaN:算 5 日均线,第 1 到第 4 天凑不齐 5 个数,自然没有结果,这不是 bug。

第六步:把结果看出来

数字看着累,画出来就直观了。matplotlib 是 Python 最常用的绘图库。

import matplotlib.pyplot as plt

# 装一下:pip install matplotlib
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(11, 7), sharex=True)

# 上图:收盘价 + 两条均线
ax1.plot(df.index, df['close'], label='Close', linewidth=1)
ax1.plot(df.index, df['ma5'], label='MA5', linewidth=1)
ax1.plot(df.index, df['ma20'], label='MA20', linewidth=1)
ax1.set_title('Simulated price with moving averages (DEMO DATA ONLY)')
ax1.legend()

# 下图:累计收益曲线
ax2.plot(df.index, df['cum_return'], label='Cumulative return')
ax2.axhline(0, color='gray', linewidth=0.8)   # 画一条 0 基准线
ax2.set_title('Cumulative return')
ax2.legend()

plt.tight_layout()
plt.show()

跑完你会看到两张图:上面是模拟收盘价配两条均线,下面是累计收益曲线。再强调一次——这是用随机数造出来的模拟价格,图形不代表任何真实标的,更不代表任何策略能赚钱。 它只是让你确认「数据流通顺了、指标算对了」。

不想画图、只想要个数字汇总也行,pandas 一行给你统计:

# describe() 给出每列的计数、均值、标准差、最大最小值等
print(df[['daily_return', 'cum_return']].describe())

一张表,记住这套清洗流程的关键动作

环节 pandas 动作 为什么这一步不能省
看数据 df.head() / df.info() 先认清字段和类型,别盲算
设时间索引 set_index('date') 让所有按时间的操作能对齐
排序 sort_index() 日期乱序会让收益率/均线全错且不报错
查缺失 isna().sum() 先知道缺多少,再决定怎么补
补缺失 ffill()(按业务选) 删行/填 0 会污染序列
算日收益 pct_change() 把价格转成可比较的涨跌幅
算累计收益 (1+r).cumprod()-1 必须连乘,不能累加
算均线 rolling(n).mean() 滚动窗口派生指标
防未来函数 shift(1) 信号指导动作必须延后一天

还有几个真实世界的坑,比代码更重要

上面讲的是「代码层面」的干净。但行情数据还有几个「数据来源层面」的坑,是再熟练的 pandas 也补不了的,必须在拿数据时就警惕。

第一个是复权问题。股票分红、送股、拆股之后,价格会「跳一下」,但这不是真涨真跌。如果你拿的是「不复权」价格,分红除权那天会凭空算出一个大跌,污染收益率。做历史分析通常要用后复权价格。这不是代码能修的,是你取数时就得选对的字段。

第二个是幸存者偏差(survivorship bias)。如果你的数据集里只剩下「现在还活着」的公司——退市的、破产的、被并购的都被剔除了——那你研究出来的任何结论都会偏乐观,因为你天然只统计了「活下来的赢家」。这就像只调查中了彩票的人,得出「买彩票能发财」的结论。

🚧 避坑

坑:以为「跑通了代码」就等于「数据是对的」。 复权、幸存者偏差这类问题,pandas 不会报任何错,代码跑得飞快、曲线漂漂亮亮,但底层数据本身就是歪的。为什么坑:技术上的干净(没有 NaN、排好序)和数据质量上的干净(口径正确、样本无偏)是两码事,后者更隐蔽、更致命。怎么避:拿到任何数据集,先问三句——价格复权了吗?退市的公司在不在里面?时间段有没有断档?想不清楚来源口径,再漂亮的回测都不能信。关于这些数学和认知层面的陷阱,可以去 原理库 系统补一补。

自检清单:开算之前先过一遍

  • 我看过 head()info(),确认每一列的含义和数据类型了吗?
  • 时间设成索引并 sort_index() 了吗?
  • 我查过缺失值的数量和位置,并按业务含义(而非偷懒)选了填充方式吗?
  • 累计收益我用的是连乘 (1+r).cumprod(),没有错用累加吧?
  • 凡是「信号决定动作」的地方,我 shift(1) 了吗?有没有偷看未来?
  • 这份价格是复权的吗?数据集有没有幸存者偏差?

这六条全过了,你的数据才算真正「干净」,后面再谈策略才有意义。

小结

  • 量化的第一步不是写策略,是把行情数据洗干净:设时间索引、排序、查补缺失值,每一步都有它非做不可的理由。
  • 收益率用 pct_change、累计收益用 (1+r).cumprod()-1连乘不是累加)、均线用 rolling().mean(),这三件是行情数据的基本派生指标。
  • 比代码更要命的是数据来源层面的坑:未来函数、复权、幸存者偏差、缺失值乱处理——它们不报错,却能让你的所有结论悄悄变成垃圾。
  • 本文全程是模拟数据的教学演示,不构成任何买卖信号。把基本功练扎实,再往后走。

继续往下,可以回到 L5阶梯 看量化工具链的整体路线;想搞懂收益率、波动率背后的数学和那些认知陷阱,去 原理库 补地基;任何涉及实盘的念头出现前,先重读一遍 /disclaimer/

📄 来源 / 自校链接

本文为投资教育整理,关键数据与结论请结合下列权威来源验证。

内容有错、看不懂、或想看下一篇?告诉我们 →

本文为投资教育整理,不构成任何投资建议;不荐个股/基金、不预测点位、不承诺收益。关键数据与结论请结合权威来源自行验证,并见风险免责声明