Tushare前复权数据实战:均线KDJ MACD指标计算避坑指南
简介这份资源面向量化投资初学者与需要自建行情数据管道的开发者围绕 tushare 接口获取股票前复权日线数据并实现均线、KDJ、MACD、RSI、CCI、MTM 等常用技术指标的计算。解压后运行 demo 即可拉取指定编码的股票数据demosession1 通过循环批量获取全部沪深标的所得前复权结果与常规股票软件显示一致便于后续回测与策略验证。压缩包共 32 个文件约 38KB以 18 个 py 脚本为核心辅以 9 个 pyc 缓存、2 个 md 说明文档及 license、gitignore、ini 配置文件指标计算代码集中在 indexes 目录tests 目录提供 ma、kdj、macd、rsi、mtm 等测试用例结构清晰、便于按模块阅读与二次开发。目前已有 7392 人学习下载适合想快速搭建股票数据获取与指标计算流程的读者参考借鉴。1. 前复权数据拿不对后面所有指标都是白算很多人做股票量化第一步就栽在数据上。用 tushare 拉日线看着 daily 接口返回的 close 挺正常直接丢进均线、KDJ、MACD 里算结果回测收益漂亮得离谱实盘一跑就废。问题往往不在指标公式而在你用的是不复权价格。某只票十送十之后K 线直接腰斩均线瞬间死叉MACD 绿柱拉满可这根本不是趋势反转是除权除息造成的价格跳空。前复权就是把这根人为的跳空补回去让历史价格和当前价格在同一把尺子上。tushare 的 pro_bar 接口支持 adjqfq 参数一行就能拿到前复权行情但真正落地时复权因子、停牌日、新股上市初期数据缺失这些细节才是让指标集体翻车的地方。这篇笔记按我自己的实操顺序从拉数据、存数据、算指标到排错把这条链路走一遍适合刚接触 tushare 想搭本地指标库的开发者也适合已经能跑通但结果对不上的熟手。2. 用 tushare pro_bar 拉前复权日线参数怎么设、数据怎么存2.1 为什么选 pro_bar 而不是 dailytushare 的 daily 接口返回的是不复权原始行情close 就是当天实际成交价。要做技术指标必须走 pro_bar它内部会根据复权因子把价格调整成前复权或后复权。前复权的含义是以最新价格为基准把历史价格按复权因子缩放使得当前价格保持真实历史价格被调整。这样算出来的均线、MACD 才是连续的。常见做法是直接调ts.pro_bar(ts_code000001.SZ, adjqfq, start_date20200101, end_date20241231)。注意 adj 参数只接受 qfq、hfq、None 三种写错会静默返回不复权数据这是第一个容易踩的坑。2.2 拉取单只股票前复权数据的完整代码import tushare as ts import pandas as pd # 初始化 pro 接口token 换成你自己的 pro ts.pro_api(你的token) def get_qfq_daily(ts_code, start_date, end_date): 获取单只股票前复权日线 ts_code: 股票代码如 000001.SZ start_date/end_date: YYYYMMDD df ts.pro_bar( ts_codets_code, adjqfq, # 前复权关键参数 start_datestart_date, end_dateend_date, freqD # 日线频率 ) if df is None or df.empty: print(f{ts_code} 返回空数据检查代码或日期区间) return None # pro_bar 返回按日期倒序改成升序方便算指标 df df.sort_values(trade_date).reset_index(dropTrue) # trade_date 转成 datetime 并设为索引 df[trade_date] pd.to_datetime(df[trade_date]) df.set_index(trade_date, inplaceTrue) # 只保留指标计算需要的列 df df[[open, high, low, close, vol, amount]] return df # 示例拉某只票 2020 到 2024 的前复权日线 data get_qfq_daily(000001.SZ, 20200101, 20241231) print(data.tail())逻辑说明pro_bar 返回的字段包含 open、high、low、close、pre_close、change、pct_chg、vol、amount。前复权只调整价格类字段vol 和 amount 不变。排序这一步不能省tushare 默认按 trade_date 降序返回直接算均线会得到反向结果。参数上freqD 是日线周线用 W月线用 M但做 MACD 和 KDJ 一般用日线周线级别需要另外处理周期对齐。2.3 批量拉取与本地存储的工程化写法单只股票手动拉没问题要做全市场回测就得批量。tushare 有积分和频率限制批量拉取必须加延时和重试。import time import os def batch_download(code_list, start_date, end_date, save_dir./qfq_data): 批量下载前复权日线并存成 csv os.makedirs(save_dir, exist_okTrue) for code in code_list: file_path os.path.join(save_dir, f{code.replace(., _)}.csv) if os.path.exists(file_path): print(f{code} 已存在跳过) continue try: df get_qfq_daily(code, start_date, end_date) if df is not None: df.to_csv(file_path) print(f{code} 保存成功共 {len(df)} 行) except Exception as e: print(f{code} 拉取失败{e}) time.sleep(0.4) # 控制频率避免触发限流 # 示例代码列表 codes [000001.SZ, 600000.SH, 000002.SZ] batch_download(codes, 20200101, 20241231)参数说明time.sleep(0.4) 是经验值普通积分账户每分钟调用次数有限太快会返回空数据或报错。存 csv 而不是数据库是为了让新手能直接用 pandas 读后续要上生产再换 parquet 或 sqlite。文件命名把点换成下划线避免路径解析出问题。提示pro_bar 在部分积分等级下对单次返回条数有限制如果发现数据只有最近几百行需要分段拉取再拼接。3. 均线、KDJ、MACD 三套指标的手写实现与参数校验3.1 均线别直接用 rolling 就完事均线本身简单df[close].rolling(5).mean()就是 5 日均线。但前复权数据里新股上市初期或者长期停牌复牌后rolling 会把停牌期间的空值算进去导致均线偏移。正确做法是先确认索引是连续交易日tushare 返回的数据本身只含有交易的日期停牌日不会出现所以 rolling 按行数滚动是合理的。但如果你的数据里混入了非交易日必须先 dropna。def add_ma(df, windows(5, 10, 20, 60)): 添加多条均线 for w in windows: df[fma{w}] df[close].rolling(windoww, min_periodsw).mean() return df data add_ma(data) print(data[[close, ma5, ma20]].tail(10))参数说明min_periodsw 表示前 w-1 行是 NaN这是正常的不要用 fillna 填 0否则前几根均线会失真。窗口选择上5/10/20/60 是常见组合做短线可以加 3 和 8做中线加 120 和 250。3.2 KDJ三个参数和两次平滑的细节KDJ 的核心是 RSV然后对 RSV 做两次指数移动平均得到 K 和 DJ 3K - 2D。很多人算出来的 KDJ 和行情软件对不上问题出在初始值和平滑方式上。国内行情软件普遍用 SMA(RSV, 3, 1) 这种带权重的平滑等价于 alpha1/3 的 EMA。def add_kdj(df, n9, m13, m23): KDJ 指标 n: RSV 周期默认 9 m1: K 值平滑周期默认 3 m2: D 值平滑周期默认 3 low_n df[low].rolling(n, min_periods1).min() high_n df[high].rolling(n, min_periods1).max() rsv (df[close] - low_n) / (high_n - low_n) * 100 # 处理分母为 0 的情况一字板 rsv rsv.fillna(50) # 等价于 SMA(rsv, m1, 1) df[k] rsv.ewm(alpha1/m1, adjustFalse).mean() df[d] df[k].ewm(alpha1/m2, adjustFalse).mean() df[j] 3 * df[k] - 2 * df[d] return df data add_kdj(data) print(data[[close, k, d, j]].tail(10))逻辑说明ewm 的 adjustFalse 是关键它让递推公式和行情软件一致。如果 adjustTrue前期数据会带偏差。rsv.fillna(50) 处理的是 high_n 等于 low_n 的情况比如连续一字涨停或跌停此时 RSV 无意义填 50 是常见做法。参数上 n9、m13、m23 是默认值改成 9/3/3 之外的值要和你的交易系统匹配不要随意调。3.3 MACDEMA 的初始值决定了前 30 根准不准MACD 由 DIF、DEA、MACD 柱三部分组成。DIF EMA12 - EMA26DEA EMA(DIF, 9)MACD 柱 2 * (DIF - DEA)。难点在 EMA 的初始值pandas 的 ewm 默认从第一行开始递推而行情软件通常用第一天的收盘价作为 EMA 起点两者在数据量足够大时趋同但前 30 根会有差异。def add_macd(df, fast12, slow26, signal9): MACD 指标 ema_fast df[close].ewm(spanfast, adjustFalse).mean() ema_slow df[close].ewm(spanslow, adjustFalse).mean() df[dif] ema_fast - ema_slow df[dea] df[dif].ewm(spansignal, adjustFalse).mean() df[macd] 2 * (df[dif] - df[dea]) return df data add_macd(data) print(data[[close, dif, dea, macd]].tail(10))参数说明span 和 alpha 的关系是 alpha 2/(span1)所以 span12 等价于 alpha2/13。adjustFalse 同样是为了对齐行情软件。如果你发现自己的 MACD 和某平台对不上先检查数据是不是前复权再检查 adjust 参数最后才怀疑公式。前复权数据每次除权后历史值会变所以昨天算的 MACD 和今天算的历史 MACD 可能不同这是正常的回测时要保证每次都用同一份复权数据。3.4 三套指标合并与输出校验def build_indicators(df): df add_ma(df, windows(5, 10, 20, 60)) df add_kdj(df) df add_macd(df) return df data build_indicators(data) # 检查是否有异常值 print(data[[ma5, k, d, dif, dea]].describe()) # 保存结果 data.to_csv(000001_SZ_indicators.csv)校验方法describe() 看 k 和 d 是否在 0 到 100 之间j 可以超出这个范围这是正常的。dif 和 dea 的均值应该接近 0。如果 k 出现负数或者大于 100检查 rsv 的计算是不是用了未复权数据。4. 避坑与排查前复权指标计算里最容易翻车的 5 个点4.1 现象均线在除权日出现巨大跳空原因用了不复权数据解决确认 adjqfq这是最典型的翻车现场。某只票 10 转 10除权日 close 从 20 变成 10不复权数据里均线直接断崖。前复权会把除权前的价格除以 2均线就连续了。排查方法很简单拉一只近期除权的票对比 daily 和 pro_bar(adjqfq) 的 close如果 pro_bar 的历史价格明显低于 daily说明复权生效了。4.2 现象KDJ 的 K 值长期贴在 0 或 100原因RSV 分母为 0 未处理解决fillna(50) 或跳过一字板连续一字涨停时high 等于 lowRSV 分母为 0pandas 会返回 inf 或 NaN。如果不处理ewm 会把 inf 传播下去导致 K 值异常。解决就是在算 rsv 之后加.replace([np.inf, -np.inf], np.nan).fillna(50)。更严谨的做法是把一字板当天的 KDJ 设为前值但实现复杂新手用 50 足够。4.3 现象MACD 前 30 根和行情软件对不上原因EMA 初始值差异解决丢弃前 60 根或手动设初始值pandas 的 ewm 从第一行开始算行情软件可能用更早的数据预热。如果你只拉了 2020 年之后的数据前几十根 MACD 必然有偏差。解决办法有两个一是多拉一年数据算完指标再截取你要的区间二是手动把第一行的 ema 设为 close然后递推。我一般用第一种省事且可靠。4.4 现象批量拉取时部分股票返回空原因代码格式错误或退市解决校验代码后缀并捕获异常tushare 的股票代码必须带 .SZ 或 .SH 后缀写成 000001 会返回空。另外退市股票在 pro_bar 里可能没有数据。批量下载时一定要 try/except把失败的代码记下来单独处理不要让它中断整个任务。4.5 现象前复权数据每天变化回测结果不可复现原因复权因子随最新价格更新解决固定数据快照前复权的基准是最新价格所以每次除权后历史数据都会被重新调整。今天拉的数据和昨天拉的数据历史价格可能不同。做回测时必须在同一天拉取全量数据并保存快照之后所有计算都用这份快照。不要每天重新拉数据然后对比历史指标那样永远对不上。5. 把指标计算封装成可复用模块一个函数搞定全流程5.1 模块化设计输入代码和日期输出带指标的 DataFrame前面都是分步写实际用的时候应该封装成一个函数减少重复代码。我一般会建一个indicator.py把数据获取和指标计算分开数据获取负责拉取和缓存指标计算只接收 DataFrame。# indicator.py import pandas as pd import numpy as np def calc_all(df): 输入包含 open/high/low/close 的 DataFrame返回带指标的 df df.copy() # 均线 for w in (5, 10, 20, 60): df[fma{w}] df[close].rolling(w, min_periodsw).mean() # KDJ low_n df[low].rolling(9, min_periods1).min() high_n df[high].rolling(9, min_periods1).max() rsv (df[close] - low_n) / (high_n - low_n) * 100 rsv rsv.replace([np.inf, -np.inf], np.nan).fillna(50) df[k] rsv.ewm(alpha1/3, adjustFalse).mean() df[d] df[k].ewm(alpha1/3, adjustFalse).mean() df[j] 3 * df[k] - 2 * df[d] # MACD ema12 df[close].ewm(span12, adjustFalse).mean() ema26 df[close].ewm(span26, adjustFalse).mean() df[dif] ema12 - ema26 df[dea] df[dif].ewm(span9, adjustFalse).mean() df[macd] 2 * (df[dif] - df[dea]) return df逻辑说明这个函数不负责拉数据只负责算这样你可以用 csv、数据库、实时接口任何来源的数据。参数都写死成常用值要改就改函数默认参数不要在调用处硬编码。5.2 用本地缓存加速重复回测每次回测都调 tushare 接口既慢又浪费积分。我的习惯是第一次拉全量存 csv之后回测直接读本地文件只有需要最新数据时才增量拉取。import os import pandas as pd def load_or_download(ts_code, start_date, end_date, cache_dir./qfq_data): 优先读本地缓存没有才下载 file_path os.path.join(cache_dir, f{ts_code.replace(., _)}.csv) if os.path.exists(file_path): df pd.read_csv(file_path, index_coltrade_date, parse_datesTrue) # 简单检查日期范围是否覆盖 if df.index.min() pd.to_datetime(start_date) and df.index.max() pd.to_datetime(end_date): return df # 缓存不满足重新下载 df get_qfq_daily(ts_code, start_date, end_date) if df is not None: os.makedirs(cache_dir, exist_okTrue) df.to_csv(file_path) return df参数说明parse_datesTrue 让 trade_date 自动解析成 datetime 索引。日期范围检查是防止缓存太旧如果缓存只到 2023 年你要回测 2024 年就得重新拉。这个逻辑可以再细化成增量更新但新手先用全量覆盖就够了。5.3 验证指标是否正确的三个土办法第一个办法拿一只你熟悉的票把算出来的 MA5 和某行情软件对比误差应该在 0.01 以内。第二个办法检查 KDJ 的 K 和 D 是否在 0 到 100 之间J 是否偶尔超出。第三个办法看 MACD 的柱状图红绿柱切换是否和价格趋势大致吻合。如果这三个都过了指标计算基本没问题。注意前复权数据在除权日附近指标会有一次整体偏移这是正常的不是 bug。回测时如果跨越除权日要确保复权基准一致。5.4 一个容易忽略的细节停牌期间的数据处理tushare 返回的日线数据不包含停牌日所以你的 DataFrame 索引是跳跃的。算均线时rolling 是按行数滚不是按自然日滚这符合技术指标的定义。但如果你要把指标对齐到自然日做可视化需要先 reindex 到交易日历再 ffill 价格但指标不要 ffill否则停牌期间会出现虚假的均线值。我的做法是可视化时直接用原始索引不强行对齐自然日。5.5 从单只股票到全市场性能上的取舍全市场 5000 多只股票每只拉 5 年日线csv 文件大概 1MB 左右总共 5GB。用 pandas 逐个读会慢可以改用 parquet 按列压缩读取速度能快 3 到 5 倍。但如果你只是做几十只票的策略csv 完全够用不要过早优化。我自己的习惯是先用 csv 跑通逻辑确认策略有效再换存储格式。希望帮到你。本文还有配套的精品资源点击获取