☰
pandas股票分析实战:从数据清洗到技术指标与回测的完整工作流
2026/10/13 13:28:48 网站建设 项目流程

做股票分析这件事,我前后换过好几套方案。最早用Excel拉数据,公式拖来拖去,数据一多就卡;后来试着写Python循环一根根算K线,代码绕得自己都看不下去。直到把pandas用顺手,才真正有了“pandas应用-股票分析”该有的样子——几十行代码就能把行情读取、数据清洗、指标计算、回撤统计全部串起来,而且每一步都能随时验证。这篇内容不是教科书式的API罗列,是我实际复用率最高的那套工作流整理,适合已经有Python基础、想把复盘和选股逻辑快速落地的人。读完你至少能搭出自己的一套日线分析模板,后续加策略信号、做批量扫描,也只是在这个框架上做加法。

1. 先想清楚数据长什么样,再谈指标计算

很多做股票分析的朋友一上来就写MACD、写布林带,结果拿着乱糟糟的数据折腾半天,最后发现指标曲线是扭曲的。我自己的习惯是:动任何计算之前,先把数据结构和口径拧清楚。

1.1 行情数据的最小字段集

pandas分析日线数据,最少需要这六列:日期、开盘价、最高价、最低价、收盘价、成交量。偶尔会加上成交额,但不是必需。对应的DataFrame长这样:

字段英文名类型建议典型用途
日期datedatetime64索引、切片、重采样
开盘价openfloat64当日跳空判断
最高价highfloat64波动区间、上影线
最低价lowfloat64支撑位、下影线
收盘价closefloat64绝大多数指标的计算基础
成交量volumeint64/float64量能配合验证

我一般用第三方开源财经数据接口拉历史数据,或者直接把股票软件导出的CSV读进来。自己构造一个示例DataFrame也很方便,方便调试:

import pandas as pd df = pd.DataFrame({ 'date': pd.date_range('2024-01-02', periods=10, freq='D'), 'open': [10, 10.2, 10.1, 10.5, 10.8, 10.7, 11.0, 11.2, 11.1, 11.5], 'high': [10.4, 10.6, 10.4, 10.9, 11.0, 11.1, 11.3, 11.4, 11.4, 11.8], 'low': [9.9, 10.0, 10.0, 10.4, 10.6, 10.6, 10.9, 11.0, 11.0, 11.3], 'close': [10.2, 10.1, 10.5, 10.8, 10.7, 11.0, 11.2, 11.1, 11.5, 11.6], 'volume': [10000, 12000, 9800, 15000, 16000, 14000, 18000, 17000, 21000, 23000] })

如果是从CSV读,最稳妥的写法是先转日期再设索引:

df = pd.read_csv('stock_demo.csv') df['date'] = pd.to_datetime(df['date']) df.set_index('date', inplace=True) df.sort_index(inplace=True)

顺序很重要:先转datetime,再设索引。很多人先set_index再做类型转换,索引类型变成object的坑就是这么来的。

1.2 为什么是pandas而不是纯列表或Excel

纯Python列表也能算均线,比如写一个for循环,把最近N天的收盘价求平均。但代码一旦涉及多个指标、多只股票、不同周期,循环方案的复杂度和出错率是几何级上升。pandas的核心优势是向量化操作,背后是C实现,同样算250日均线,pandas一行解决,速度是Python循环的几十倍以上。

这一点用生活类比最好理解:纯列表操作就像你拿计算器一笔笔按,算完20个数字手已经酸了;pandas则像Excel里拉好公式,数据一变,整列自动刷新。尤其后面要算累计净值、最大回撤、信号平移,pandas的shift、cumprod、rolling这些方法会让代码短到难以置信,而且可读性比循环强太多。

1.3 复权或不复权,决定你的指标真假

股票存在除权除息,比如一只股票10元,10派5之后股价会变成5元左右,K线上会留下一个大缺口。如果你直接用不复权数据算均线、算MACD,这个缺口会被当成暴跌,指标完全失真。关于复权我多说几句:

  • 不复权:是真实成交价,但对技术指标最不友好。
  • 前复权:以当前价格为基准,把历史价格往下调,保证历史走势连续,这是做股票指标分析最常用的口径。
  • 后复权:以最早价格为基准,后复权价格适合做长期收益率计算,因为它不会因为未来发生新的除权而改变历史数值。

通俗点说,前复权是“站在今天回头看过去”,后复权是“站在起点看未来”。做日常选股和技术指标,直接用前复权;做长期收益统计、回测,用后复权更稳。数据接口一般有复权参数,拉数据时务必确认好,别拿不复权数据硬算指标,这是新手最容易踩的坑,也是我强调“先看数据再算指标”的原因。

2. 数据清洗和时间索引,90%的脏数据问题都出在这

拿到行情数据的第一件事不是算指标,而是检查数据到底干不干净。我遇到过的情况太多了:重复日期、缺失收盘价、日期格式混搭、成交量单位不统一、甚至有些CSV的数值列里藏着空格和中文逗号。这些都是事前5分钟能解决的问题,事后却能让你排查一整天。

2.1 一套可以直接套用的清洗流程

我每次拿到新数据源,都会按下面这套顺序处理,实测最稳:

# 1. 转日期并设为索引 df['date'] = pd.to_datetime(df['date']) df.set_index('date', inplace=True) df.sort_index(inplace=True) # 2. 去除完全重复的行 df = df[~df.index.duplicated(keep='last')] # 3. 关键列缺失值直接删,别用填充 df = df.dropna(subset=['open', 'high', 'low', 'close']) # 4. 把价格和成交量转成数值类型,顺便把文本里的逗号清掉 for col in ['open', 'high', 'low', 'close', 'volume']: df[col] = pd.to_numeric(df[col].astype(str).str.replace(',', ''), errors='coerce')

这里有个细节:为什么缺失值直接用dropna而不是fillna?因为股票数据只要某天收盘缺失,意味着那天大概率停牌或者数据源本身有问题,你没法用前一天的收盘价去伪造,这会直接扭曲后续指标。真实分析中我只会对成交量做有限填充,价格绝不补值。

2.2 日期切片和周期重采样,熟练之后效率翻倍

清洗干净后,日期索引的威力才真正显示出来。切片特别直白:

# 取2024年上半年的数据 df_slice = df.loc['2024-01-01':'2024-06-30'] # 取某一天的某几列 df.loc['2024-03-15', ['close', 'volume']]

如果你做周线或月线级别分析,不需要重新拉数据,直接用resample聚合:

df_weekly = df.resample('W-FRI').agg({ 'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last', 'volume': 'sum' }) df_monthly = df.resample('M').agg({ 'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last', 'volume': 'sum' })

注意周线我用的W-FRI,这是国外行情软件的习惯,以周五作为一周的结束日。如果你用的是国内周线口径,直接W也可以,两者差别不大。但月度重采样用M是日历月末,不是最后一个交易日,如果更严谨,可以先用'BM'(工作日月末)。这类细节不会报错,但会悄悄影响你的聚合结果。

2.3 时间戳和时间区间的坑

如果你分析的是日线,日期基本是干净的。一旦碰期货或分钟线,时间戳问题就冒出来了。常见的情况是:某个数据源返回的时间带时区,比如2024-01-02 15:00:00+08:00,而另一个不带,两个数据源合并时pandas类型不一致,直接报错或者产生NaN。这时候统一处理:

df.index = pd.to_datetime(df.index, utc=True).tz_convert('Asia/Shanghai')

实际项目中,我会建议所有人把日期操作集中在一个函数里,统一处理完之后再进业务逻辑。别在主流程里到处写时间转换,后续维护会让你怀疑人生。

3. 核心指标计算:从均线到回撤,一整套可复用代码

数据干净了,接下来才是真正好玩的部分。这一章我用真实项目中跑过的代码,逐个拆解移动平均线、MACD、RSI、收益率和最大回撤的pandas实现。每个指标都会解释为什么用这个方法算,以及参数背后的直觉。

3.1 移动平均线:rolling窗口的“滑窗”逻辑

均线是技术分析的基础,也是pandas滚动窗口最典型的应用。rolling(N)的意思是维护一个长度为N的滑窗,逐个位置滚动计算窗口内的统计值:

df['sma5'] = df['close'].rolling(5).mean() df['sma10'] = df['close'].rolling(10).mean() df['sma20'] = df['close'].rolling(20).mean() df['sma60'] = df['close'].rolling(60).mean()

为什么用5、10、20、60?国内常用周线、双周、月线、季线的近似交易日数。你完全可以根据自己的交易周期选择,比如做超短用3日、5日,做波段用20日、60日。

有了均线,金叉死叉信号就是两列比较:

df['signal'] = 0 df.loc[df['sma5'] > df['sma20'], 'signal'] = 1 df.loc[df['sma5'] < df['sma20'], 'signal'] = -1

但这里有个关键坑:直接用当日均线比较,等于你当天收盘才知道信号,却假设当天开盘就执行了,这是典型的“未来函数”。正确做法是把信号整体向后平移一天,用昨天的信号决定今天的操作:

df['signal_shift'] = df['signal'].shift(1)

金叉死叉模型后续可以做成一个简单策略:持仓时每日收益等于股票日收益,空仓时收益为0。信号平移之后,统计出的策略净值曲线才接近真实可交易的结果。

3.2 MACD:ewm指数加权到底在算什么

MACD其实不复杂,不需要当成什么高深指标。它由快线DIF、慢线DEA和柱状图构成:

df['ema12'] = df['close'].ewm(span=12, adjust=False).mean() df['ema26'] = df['close'].ewm(span=26, adjust=False).mean() df['dif'] = df['ema12'] - df['ema26'] df['dea'] = df['dif'].ewm(span=9, adjust=False).mean() df['macd_bar'] = (df['dif'] - df['dea']) * 2

ewm和rolling的区别在于:rolling窗口内每个点权重一样,ewm越近的数据权重越大。这更贴近“近期价格更重要”的交易直觉。参数12、26、9是国内外软件默认的经典参数,一般不需要改。国内很多软件的MACD柱状图数值和dif - dea差2倍,所以代码里乘了2,如果你要和行情软件对照,就保留这个系数,否则去掉也不影响逻辑。

MACD的信号逻辑通常是:DIF上穿DEA为金叉,下穿为死叉;或者观察柱状图由负转正。实现方式和均线信号基本一样,不再重复。

3.3 相对强弱指标RSI:涨跌幅平滑后的比值

RSI衡量一段时间内上涨力量相对下跌力量的强弱,pandas实现有很多版本,我用的是最接近主流行情软件的Wilder平滑法:

window = 14 delta = df['close'].diff() gain = delta.clip(lower=0) # 只保留上涨部分 loss = -delta.clip(upper=0) # 只保留下跌部分 avg_gain = gain.ewm(alpha=1/window, adjust=False).mean() avg_loss = loss.ewm(alpha=1/window, adjust=False).mean() rs = avg_gain / avg_loss df['rsi14'] = 100 - (100 / (1 + rs))

代码里的alpha=1/window对应的是Wilder的平滑系数,和普通ewm的span不太一样。如果avg_loss等于0,说明这段时间没有下跌,RSI自然为0到100之间,实际显示会是100或接近100。

RSI的常见用法是超买超卖:超过70算超买,低于30算超卖。但实际用起来我会加一个逆向思维——强势股RSI可以在80以上钝化很久,弱势股RSI在20以下趴着不动,直接机械式抄底逃顶会被市场反复教育。RSI更适合作为“确认工具”而不是“触发信号”。

3.4 收益率、累计净值和最大回撤:pandas最值得学的组合

分析股票,最终都要把价格变成收益率。日收益率两种算法我都用:

import numpy as np # 简单收益率 df['ret'] = df['close'].pct_change() # 对数收益率 df['log_ret'] = np.log(df['close'] / df['close'].shift(1))

简单收益率直接相减再除以前值,对数收益率的优势在于时间上的可加性,长期收益率的计算会更方便。日常分析和可视化建议用简单收益率,做统计建模时用对数收益率。

累计净值曲线,一段就能搞定:

df['cum_nav'] = (1 + df['ret'].fillna(0)).cumprod()

这行代码翻译成人话就是:把每天的收益连乘起来,得到“如果期初投入一块钱,现在值多少钱”。有了净值曲线,最大回撤就是最高点到最低点的最大跌幅:

df['cum_max'] = df['cum_nav'].cummax() df['drawdown'] = df['cum_nav'] / df['cum_max'] - 1 max_drawdown = df['drawdown'].min()

max_drawdown是负数,比如-0.25意味着历史上最大回撤了25%。这个数对实盘心理承受力评估极其重要。算的时候务必保证cum_max是当前时刻之前的最高净值,pandas的cummax做的就是动态累积最大值,天然满足这个需求,不需要写循环。

4. 可视化与信号验证,别让指标只停留在数据表里

很多初学者算出一堆指标,却只会在终端里print看数字,这效率太低,也看不出规律。pandas直接集成了绘图能力,底层调用matplotlib,日常分析完全够用。

4.1 价格和均线叠加,一眼看出趋势

import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei'] # 解决中文乱码 plt.rcParams['axes.unicode_minus'] = False # 解决负号乱码 df[['close', 'sma5', 'sma20', 'sma60']].plot(figsize=(10, 6), grid=True) plt.title('close vs SMA') plt.show()

视觉上你会立刻看到:收盘价站上均线、均线多头排列或空头排列,这些在数据表里不明显的结构,图上非常直观。我还会把成交量画在子图里,辅助判断放量还是缩量:

fig, axes = plt.subplots(2, 1, figsize=(10, 8), sharex=True) df[['close', 'sma20']].plot(ax=axes[0], grid=True) df['volume'].plot(ax=axes[1], kind='bar', grid=True)

4.2 把买卖信号变成可验证的数值

画出信号图是第一步,更硬核的做法是把信号翻译成策略收益,做一个简单的“回测”:

# 用均线策略做演示 df['position'] = df['signal_shift'].fillna(0) # 持仓状态,1或-1,空仓为0 df['strategy_ret'] = df['ret'] * df['position'] df['strategy_nav'] = (1 + df['strategy_ret'].fillna(0)).cumprod() # 统计基础指标 total_ret = df['strategy_nav'].iloc[-1] - 1 annual_vol = df['strategy_ret'].std() * np.sqrt(252) sharpe = df['strategy_ret'].mean() / df['strategy_ret'].std() * np.sqrt(252)

这段代码的逻辑:signal_shift是昨天生成的信号,昨天说1就今天持有,涨跌幅就能吃完全;昨天说-1就持有反向,就做空;说0就空仓,收益为0。夏普比率用年化波动率做了换算,如果年化夏普大于1,勉强算得上一个不烂的策略。这里没有考虑手续费和滑点,实盘一定要加上,否则你算出来的收益会偏乐观。

4.3 批量扩展到多只股票

单只股票看得再准,也需要横向对比。用pandas做多股票扫描很简单,维护一个字典存每只股票的收盘序列,然后拼成一个DataFrame:

price_dict = { 'stock_A': df_a['close'], 'stock_B': df_b['close'], 'stock_C': df_c['close'], } price_df = pd.DataFrame(price_dict) price_df.pct_change().corr()

相关性矩阵能帮你在一个板块里找到走势高度同步的股票。进一步,你可以对所有股票批量算均线、算RSI,然后筛选出满足条件的股票池。这才是“pandas应用-股票分析”最实用的场景:把自己从盯盘里解放出来,让代码干活。

5. 高频踩坑实录:这些问题我几乎每次都遇到

这一章是长期用pandas分析股票之后沉淀的排查经验。很多坑在研究阶段不致命,但一旦上量、上时间周期或者换数据源,就会莫名其妙地爆发。整理成一套速查表,再展开讲几个高发问题。

症状可能的根因快速排查方法
日期索引看起来是日期,其实是字符串未用to_datetime,set_index顺序错了df.index.dtype
指标从某天开始全变NaNrolling窗口过短或数据存在缺失值检查rolling参数、dropna结果
计算结果比行情软件差一倍复权口径不一致或MACD柱没乘2对比close列和行情软体数据
收益率出现极端值数据里有0或异常价格df[df['close'] <= 0]
两个数据源合并后全是NaN时间戳时区不一致统一转为UTC再本地化
信号回测收益异常高用了未来函数,信号没有shift检查signal_shift

5.1 日期索引类型翻车

这是最容易判断也最常发生的问题。很多人读入数据后立刻set_index('date'),却不先做pd.to_datetime,导致索引类型是object。表面上看都是2024-01-02,但df.loc['2024-03-01': '2024-04-01']会直接报错或者返回空值,因为字符串按照字典序比较而不是日期比较。

我的习惯是固定用df.index = pd.to_datetime(df.index)做一次兜底转换,无论从CSV还是接口读,都统一走这一步。然后在清洗流程末尾检查:

print(df.index.dtype) # 应该是 datetime64[ns]

5.2 未来函数:回测里的隐形杀手

未来函数这个词听着玄,实际就是“量化交易中用了未来才知道的数据”。我之前做均线策略的例子非常典型:用当日收盘后的均线关系生成信号,却假设当天开盘就建仓,收益自然虚高。修复办法已经说过,信号shift(1)。

还有一个经常被忽视的位置:RSI、MACD这类指标本身用到的是当天的收盘数据,如果你在当天盘中就想判断“今天是否金叉”,那必须用实时尚未收盘的价格去推,和盘后数据算出来的是两回事。回测时别把盘后计算的指标直接当成盘中实时信号,这块会让你的回测结果和实盘差距巨大。

5.3 复权口径不一致,导致指标完全对不上

如果你拉数据用的接口默认不复权,而你朋友的软件用的是前复权,两个人分析同一只股票,均线可能完全不同。更麻烦的是,如果某只股票中间发生了大比例分红,不复权数据会在某一天突然跳水,MACD、RSI都会给出荒谬的卖空信号。谁看到都会心里一慌。

所以每一轮分析开始前,第一件事是确定数据源复权参数。我用的是:技术指标和信号计算统一用前复权;而做长期收益统计、年化收益率计算时用后复权。一旦选定就不要中途更换,否则整个分析结果内部都不用。

5.4 成交量单位不统一和除零现象

有的数据源成交量单位是股,有的是手(1手=100股),还有的是万元。如果你批量拼接多个股票,不统一单位,后面画图和量能对比全是错的。我的办法是在清洗阶段就统一成手或统一成股,并写好注释,防止自己过两天看代码时被自己搞晕。

除零一般出现在计算RSI或者其他比率指标时。不要等到出现inf或NaN再慌,计算时主动处理:

df['ret'].replace([np.inf, -np.inf], np.nan, inplace=True)

5.5 内存和性能:数据量一大,别写循环

如果只是几千行日线,性能问题完全不用考虑。但如果你想分析全市场几千只股票的分钟数据,内存和速度就会成为瓶颈。这时pandas有几个省内存的招数:

# 只读取需要的列 df = pd.read_csv('large.csv', usecols=['date', 'close', 'volume']) # 对价格和成交量用float32 df['close'] = df['close'].astype('float32') # 能向量化就别用iterrows # 实在要遍历,用itertuples比iterrows快很多

高频数据的话,用polars或者duckdb做底层存储和查询,再配合pandas做上层分析是更优解。如果你只是做日线股票分析,pandas完全扛得住。

6. 把分析流程模板化,省下重复造轮子的时间

到这一步,基础功能基本齐了。我最后分享一点让工作流更进一步的经验:把你的分析过程整理成模板,用函数封装,这样每次拿到新股票,只需要几行代码就能输出完整报告。

6.1 构建一个可复用的分析函数

我习惯把整条链路包装成一个函数,输入是行情DataFrame,输出是计算好的指标表和一份关键统计摘要:

def analyze_stock(df, sma_windows=(5, 20, 60), rsi_window=14): df = df.copy() for w in sma_windows: df[f'sma{w}'] = df['close'].rolling(w).mean() df['ema12'] = df['close'].ewm(span=12, adjust=False).mean() df['ema26'] = df['close'].ewm(span=26, adjust=False).mean() df['dif'] = df['ema12'] - df['ema26'] df['dea'] = df['dif'].ewm(span=9, adjust=False).mean() df['ret'] = df['close'].pct_change() df['cum_nav'] = (1 + df['ret'].fillna(0)).cumprod() df['drawdown'] = df['cum_nav'] / df['cum_nav'].cummax() - 1 return df

配合管道式的过滤,还可以做选股扫描:

def filter_strong_stock(df): latest = df.iloc[-1] cond1 = latest['sma5'] > latest['sma20'] cond2 = latest['sma20'] > latest['sma60'] cond3 = latest['rsi14'] > 50 return cond1 and cond2 and cond3

把条件拆成独立函数,比堆砌几百行if逻辑清晰太多。我现在的做法是:分析模板负责计算,选股规则负责筛选,两者解耦。规则可以随时改,不需要动底层计算逻辑。

6.2 多指标、多周期联动的思路

单一指标有个通病:每种指标都有自己的盲区。均线在震荡市反复打脸,MACD在窄幅行情里频繁金叉死叉,RSI在单边趋势里容易钝化。所以我实际使用时会做“共振”判断:日线均线多头排列,同时周线MACD在零轴上方,再叠加RSI没有超买,三个维度都通过才考虑介入。这种思路不复杂,就是利用pandas先算好日线和周线指标,再在选股函数里做交叉验证。

6.3 最后再分享一个小技巧

做股票分析的数据经常会遇到停牌日,也就是某几天完全没有交易数据。如果不处理直接画图、算指标,停牌前后的收益会算错,因为缺口被当成一天完成。一个比较简单的处理方式是按连续交易日重新建立日期索引,缺失交易日填充NaN,然后用前值填充价格,成交量填0。但如果你只是做日线常规指标,大多数情况下保持原始行情数据、不去重采样成自然日就足够,别给自己加戏。

我个人在实际操作中的体会是:pandas做股票分析,难的不是函数用不熟,而是数据口径和信号验证这两件事。把上面这些问题都理顺之后,剩下的就是不断叠加自己的交易理解。技术指标再花哨,也只是帮你把主观感受变成可回溯、可验证的数字,真正的决策还得靠你对市场和标的的理解。希望这套工作流能让你少走一些我走过的弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询