A股历史数据获取与复权处理实战:从接口拉取到回测验证的完整源码解析
简介这份源码资源面向个人投资者、量化初学者与金融数据研究者聚焦A股历史行情获取这一常见需求整理了Tushare、AkShare与Baostock三种免费数据接口的调用方式与示例代码帮助读者绕开付费数据源快速搭建本地行情数据库。压缩包共5个文件约8KB包含py主程序脚本、md说明文档、html演示页面、txt依赖清单及inscode配置文件分别承担接口调用示例、使用说明、可视化引导与运行环境声明等用途结构轻量、便于直接运行与二次修改。目前已有1328人学习下载说明该方案在量化入门群体中具有一定参考价值。读者可从中获得各平台注册与登录流程、日线级别数据拉取代码、字段与参数说明以及数据质量与使用限制的提醒适合作为投资分析或研究工作的起步工具也可据此对比不同接口的覆盖范围与调用成本选择契合自身需求的数据方案。1. 从一次数据断供说起A股历史数据获取到底难在哪去年帮一个做量化策略的朋友排查回测偏差跑出来的年化收益比实盘高了将近一倍。查了两天最后发现问题出在数据源上——他用的那份日线数据里前复权价格在除权日附近出现了跳空缺口而回测框架默认按连续价格计算持仓成本导致每次分红送股都被算成了一笔“无风险收益”。这不是策略的问题是数据的问题。A股历史数据的获取表面上看无非是找个接口把K线拉下来但真正落到量化交易的生产环境里坑远比想象中多。数据源是否稳定、复权因子怎么处理、停牌期间怎么填充、退市股票怎么保留每一个细节都会直接影响回测结果的可信度。这份项目源码围绕的就是这件事用代码把A股历史行情数据从公开接口拉取、清洗、存储成可以直接喂给回测引擎的格式。它适合正在搭建自己数据管线的量化开发者也适合那些用现成数据包跑策略但总觉得“哪里不对”的人。下面我会把这份源码拆开从接口选型到落地存储把能踩的坑一个个标出来。2. 数据接口选型与源码结构拆解2.1 为什么是这几个接口而不是别的做A股数据获取绕不开几个主流方案Tushare、AkShare、Baostock、以及各券商开放的行情接口。这份源码里主要用的是AkShare和Baostock的组合原因很实际——AkShare覆盖的品种全、更新快适合拉取日线和分钟线Baostock的复权因子处理得比较干净而且不需要token适合做交叉验证。Tushare Pro的积分门槛对个人开发者不太友好很多高频接口需要攒到一定积分才能调用。券商接口虽然数据质量高但通常绑定交易账户拿来跑批量历史数据拉取不太合适。源码里没有用任何需要付费或注册的接口这一点对想快速复现的人来说比较友好。具体到数据字段源码拉取的核心字段包括日期、开盘价、最高价、最低价、收盘价、成交量、成交额、换手率、前复权因子、后复权因子。其中复权因子是很多人容易忽略的——如果你只拉价格不拉因子后面做回测时遇到除权除息就会出问题。2.2 源码目录结构与核心模块拿到源码包后先别急着跑。花五分钟看一下目录结构能省掉后面很多“文件找不到”的低级错误。典型的目录布局是这样的astock_data/ ├── config/ │ └── settings.py # 接口配置、股票池、日期范围 ├── fetcher/ │ ├── ak_fetcher.py # AkShare数据拉取 │ ├── bs_fetcher.py # Baostock数据拉取 │ └── base_fetcher.py # 抽象基类统一接口 ├── cleaner/ │ ├── adjust.py # 复权处理 │ └── validator.py # 数据校验 ├── storage/ │ ├── csv_writer.py # CSV落盘 │ └── sqlite_writer.py # SQLite入库 ├── utils/ │ ├── logger.py # 日志 │ └── retry.py # 重试装饰器 └── main.py # 入口这个结构的好处是职责分离。fetcher只管拉数据cleaner只管清洗storage只管存。你想换数据源只需要在fetcher里加一个模块不用动其他部分。2.3 拉取日线数据的完整代码路径以拉取单只股票日线为例源码里的调用链是这样的main.py读取settings.py里的股票池和日期范围然后依次调用ak_fetcher的fetch_daily方法拿到原始DataFrame后传给cleaner做复权处理和缺失值检查最后写入SQLite。# fetcher/ak_fetcher.py import akshare as ak import pandas as pd from utils.retry import retry class AkFetcher: def __init__(self, adjustqfq): # adjust: qfq前复权, hfq后复权, 不复权 self.adjust adjust retry(max_attempts3, delay2) def fetch_daily(self, symbol: str, start: str, end: str) - pd.DataFrame: # symbol格式为000001不带交易所前缀 df ak.stock_zh_a_hist( symbolsymbol, perioddaily, start_datestart, end_dateend, adjustself.adjust ) if df is None or df.empty: raise ValueError(f空数据: {symbol}) # 统一列名方便后续处理 df.columns [ date, open, close, high, low, volume, amount, amplitude, pct_change, change, turnover ] df[symbol] symbol return df这段代码有几个关键点。adjust参数控制复权方式前复权适合看历史走势后复权适合做回测。retry装饰器负责在接口超时或返回空数据时自动重试最多三次每次间隔两秒。列名统一这一步很重要因为AkShare返回的列名是中文不同版本可能略有差异统一成英文后下游处理就不用反复改。参数方面start和end的格式是YYYYMMDD比如20200101。如果你要拉全市场几千只股票建议按行业或市值分批不要一次性全拉——接口有频率限制拉太快会被临时封禁。2.4 复权处理的实现细节复权是A股数据里最容易翻车的地方。简单说前复权是以当前价格为基准往前调整历史价格会变后复权是以上市首日为基准往后调整当前价格会变。源码里用的是“拉取不复权价格单独拉复权因子本地计算”的方式而不是直接依赖接口返回的复权价。# cleaner/adjust.py import pandas as pd def apply_adjust(df: pd.DataFrame, factors: pd.DataFrame) - pd.DataFrame: # factors包含date和factor两列 df df.merge(factors, ondate, howleft) df[factor] df[factor].ffill().fillna(1.0) # 后复权价格乘以因子 for col in [open, high, low, close]: df[f{col}_hfq] df[col] * df[factor] return df这样做的原因是接口直接返回的复权价在不同数据源之间可能有细微差异而自己用因子算是可验证的。因子列用ffill前向填充是因为除权日当天因子会跳变非除权日保持不变。如果你发现回测里某只股票在除权日附近收益异常大概率就是复权没处理对。3. 批量拉取与本地存储的工程化落地3.1 股票池构建与分批策略单只股票拉数据没什么难度难的是全市场几千只股票怎么高效拉完还不被封。源码里的做法是先构建股票池然后按市值或行业分成若干批每批之间加随机延时。# main.py 片段 import time import random from fetcher.ak_fetcher import AkFetcher from storage.sqlite_writer import SQLiteWriter def batch_fetch(symbols, start, end, batch_size50): fetcher AkFetcher(adjust) writer SQLiteWriter(data/astock.db) for i in range(0, len(symbols), batch_size): batch symbols[i:ibatch_size] for sym in batch: try: df fetcher.fetch_daily(sym, start, end) writer.write_daily(sym, df) except Exception as e: print(f失败 {sym}: {e}) # 每批之间随机等待降低被封风险 time.sleep(random.uniform(3, 6))batch_size设成50是一个经验值。太小拉得慢太大容易触发频率限制。随机延时比固定延时更安全因为固定间隔的请求模式更容易被识别。失败的单只股票不会中断整个批次而是记录日志后继续最后可以单独补拉。3.2 SQLite存储表结构设计存数据看起来简单但表结构设计不好后面查询会非常痛苦。源码里用了三张表日线行情表、复权因子表、股票元信息表。CREATE TABLE daily_price ( symbol TEXT NOT NULL, date TEXT NOT NULL, open REAL, high REAL, low REAL, close REAL, volume REAL, amount REAL, turnover REAL, PRIMARY KEY (symbol, date) ); CREATE TABLE adjust_factor ( symbol TEXT NOT NULL, date TEXT NOT NULL, factor REAL, PRIMARY KEY (symbol, date) ); CREATE TABLE stock_info ( symbol TEXT PRIMARY KEY, name TEXT, industry TEXT, list_date TEXT, delist_date TEXT );主键用(symbol, date)联合主键既保证了唯一性又天然建立了查询索引。日线表不存复权价只存原始价格和因子复权在查询时计算。这样做的好处是数据量小、灵活你想用前复权还是后复权写个SQL就能切换。3.3 增量更新与断点续传全量拉一次数据可能要几个小时如果中途断了从头再来心态容易崩。源码里实现了增量更新每次拉取前先查本地库里已有数据的最大日期只拉这个日期之后的部分。def get_last_date(writer, symbol): row writer.query( SELECT MAX(date) FROM daily_price WHERE symbol?, (symbol,) ) return row[0] if row and row[0] else 19900101 def incremental_fetch(symbols, end): for sym in symbols: last get_last_date(writer, sym) if last end: continue start (pd.to_datetime(last) pd.Timedelta(days1)).strftime(%Y%m%d) df fetcher.fetch_daily(sym, start, end) writer.write_daily(sym, df)这段逻辑的核心是get_last_date它从本地库查出该股票已有数据的最后日期然后从次日开始拉。注意这里有个边界如果最后日期是周五次日是周六接口会返回空数据所以实际使用时建议在日期计算后加一个交易日判断或者直接容忍空数据跳过。3.4 数据校验拉完不等于能用数据拉下来只是第一步校验才是保证质量的关键。源码里的validator模块做了几件事检查价格是否为负、检查高低价关系是否合理、检查成交量是否为零、检查日期是否有重复。def validate_daily(df): errors [] if (df[[open,high,low,close]] 0).any().any(): errors.append(存在负价格) if (df[high] df[low]).any(): errors.append(最高价低于最低价) if df[date].duplicated().any(): errors.append(日期重复) if (df[volume] 0).sum() len(df) * 0.3: errors.append(超过30%的交易日成交量为零疑似停牌未标记) return errors最后一条检查特别重要。A股停牌期间接口可能返回成交量0但价格不变的数据如果不标记出来回测时会被当成正常交易日处理导致持仓无法买卖。常见做法是在日线表里加一个is_trading字段停牌日标记为0回测引擎跳过这些日期。4. 避坑与常见问题排查4.1 接口返回空数据但股票明明在交易现象拉某只股票某段时间的日线接口返回空DataFrame但该股票在交易所正常交易。原因AkShare的stock_zh_a_hist接口对股票代码的格式有要求必须是纯数字如000001不能带SZ或SH前缀。另外如果股票在这段时间内发生过更名或代码变更旧代码可能查不到数据。解决统一股票代码格式去掉交易所前缀。对于更名股票用当前的代码去查历史数据接口通常会自动关联。如果还是空换Baostock交叉验证一下确认是接口问题还是数据本身缺失。4.2 复权因子在除权日出现跳变导致回测收益异常现象回测某只高分红股票时除权日当天收益突然跳升明显不符合实际。原因复权因子在除权日会发生变化如果因子没有正确对齐到日期或者用了前复权价格但因子没同步更新就会导致价格序列出现断层。解决不要混用接口返回的复权价和自己算的因子。要么全用接口复权价要么全用原始价加因子。源码里用的是后者因子从Baostock单独拉取按日期merge到行情数据上确保对齐。4.3 批量拉取到一半被限流现象拉了几百只股票后接口开始返回超时或空数据等一段时间又恢复。原因接口有频率限制短时间内请求过多会触发临时封禁。固定间隔的请求模式比随机间隔更容易被识别。解决降低batch_size增加随机延时。源码里默认每批50只批间延时3到6秒随机。如果还是被限把batch_size降到20延时加到5到10秒。另外把拉取任务放在非交易时段跑接口压力小成功率更高。4.4 SQLite并发写入报database is locked现象多线程拉数据时写入SQLite报database is locked错误。原因SQLite默认的锁机制不支持高并发写入多个线程同时写同一张表会互相阻塞。解决最简单的方法是改成单线程写入拉取可以用多线程但写入串行化。如果一定要并发写可以每个线程写独立的临时库最后合并。源码里用的是单线程写入加队列的方式拉取和写入解耦写入端单线程消费队列。4.5 停牌股票数据缺失导致回测持仓无法平仓现象回测时某只股票停牌期间仍有持仓但数据里没有停牌标记引擎按正常价格计算市值复牌后价格跳空导致收益异常。原因接口返回的日线数据在停牌期间可能直接缺失或者返回成交量0但价格不变的数据没有明确的停牌标记。解决在数据校验阶段识别停牌日。判断逻辑是如果某交易日成交量为0且价格与前一交易日相同标记为停牌。在日线表里加is_trading字段回测引擎读取时跳过停牌日持仓按停牌前最后价格计算。5. 从数据到回测一个可复现的验证流程数据拉完了怎么确认它真的能用我的习惯是跑一个最小验证流程选一只分红送股比较多的股票拉取包含除权日的区间数据分别用不复权、前复权、后复权三种方式计算区间收益然后和行情软件上的数据对比。import sqlite3 import pandas as pd conn sqlite3.connect(data/astock.db) # 拉取某只股票2020年到2023年的数据 df pd.read_sql( SELECT * FROM daily_price WHERE symbol000001 AND date BETWEEN 20200101 AND 20231231 ORDER BY date, conn ) factors pd.read_sql( SELECT * FROM adjust_factor WHERE symbol000001 ORDER BY date, conn ) # 不复权收益 raw_return df[close].iloc[-1] / df[close].iloc[0] - 1 # 后复权收益 df df.merge(factors, ondate, howleft) df[factor] df[factor].ffill().fillna(1.0) df[close_hfq] df[close] * df[factor] hfq_return df[close_hfq].iloc[-1] / df[close_hfq].iloc[0] - 1 print(f不复权收益: {raw_return:.2%}) print(f后复权收益: {hfq_return:.2%})跑完之后把后复权收益和行情软件上同一区间的“后复权涨幅”对比。如果差异在0.5%以内说明数据处理基本正确。如果差异大优先检查因子对齐和除权日处理。还有一个验证技巧选一只经历过10送10的股票看除权日当天不复权价格是否腰斩后复权价格是否连续。如果后复权价格在除权日出现跳空说明因子没算对。参数方面验证区间建议至少包含一次分红和一次送股否则测不出复权处理的问题。股票选择上银行股分红稳定但送股少中小板股票送股多但数据可能不全可以各选一只交叉验证。从那以后我每次拉完新数据都会先跑一遍这个验证流程确认复权处理没问题再喂给回测引擎。数据这件事宁可多花半小时验证也别等回测跑出漂亮曲线之后才发现是数据在骗你。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

Y-Net源码学习:双编码器图像分割实战与避坑指南

Y-Net源码学习:双编码器图像分割实战与避坑指南

简介:这份资源是Y-Net光声成像重建网络的完整源代码包,面向从事医学图像处理、深度学习重建研究的学生与开发者,帮助理解如何用神经网络替代传统傅立叶逆变换或有限元方法,实现快速、高质量的光声图像重建。压缩包共9个文件&#…

2026/10/12 18:47:12 阅读更多 →
OOA-XGBoost:鱼鹰算法优化XGBoost分类预测的Matlab实现

OOA-XGBoost:鱼鹰算法优化XGBoost分类预测的Matlab实现

简介:OOA-XGBoost鱼鹰算法优化XGBoost分类预测Matlab完整源码与数据,定位为面向高校理工科学生的智能优化与分类预测一体化项目模板,适合课程设计、期末大作业、毕业设计及科研算法对比。压缩包共18个文件,包含8个m源码文件&#…

2026/10/12 18:47:10 阅读更多 →
连续邮资问题:动态规划与回溯搜索的工程实践

连续邮资问题:动态规划与回溯搜索的工程实践

1. 连续邮资问题到底是什么,为什么值得花时间搞懂连续邮资问题(Consecutive Postage Problem)是组合数学与动态规划交叉领域里一个非常经典的模型。它的核心描述其实很朴素:给定 n 种不同面值的邮票,每种面值最多允许贴…

2026/10/12 18:46:08 阅读更多 →

最新新闻

元宝    LeetCode 332. Reconstruct Itinerary Java Implement

元宝 LeetCode 332. Reconstruct Itinerary Java Implement

Core idea: Hierholzer’s algorithm for an Eulerian path. A naive lexical greedy walk can get stuck (e.g. “JFK→KUL” dead-ends when “JFK→NRT→JFK→KUL” is the answer). The fix: keep walking greedily, but only append each airport on the way back up — d…

2026/10/13 10:21:51 阅读更多 →
Android Room数据库升级迁移实战:从原理到避坑指南

Android Room数据库升级迁移实战:从原理到避坑指南

1. 从一次数据库迁移事故说起:为什么Room升级值得单独拎出来讲做Android开发的人,几乎都经历过这样的场景:App上线了一个版本,本地数据库表结构设计得不太合理,产品经理又提了新需求,需要加字段、改类型、拆…

2026/10/13 10:21:51 阅读更多 →
C语言复习(内存、结构体)

C语言复习(内存、结构体)

结构体存储有int类型的存在故这个结构体的存储是12个字节(9不是整数倍),类型的顺序不一样占用内存大小不同2.内存管理:内存管理的注意事项:内存泄露、

2026/10/13 10:21:51 阅读更多 →
源码编译实战:从configure到定制安装的完整指南

源码编译实战:从configure到定制安装的完整指南

干了这么多年开发和运维,我慢慢发现一个规律:凡是对系统里某个软件有特殊要求的人,最终都会走到“源码编译”这条路上来。官方仓库下载即用的二进制包确实省事,可一旦你想换个编译参数、加一个官方版本里没带的功能模块&#xff0…

2026/10/13 10:21:51 阅读更多 →
OpenMuse:轻量级Personal Agent框架的工程实践与扩展指南

OpenMuse:轻量级Personal Agent框架的工程实践与扩展指南

1. 项目概述:OpenMuse 是什么,又不止是什么个人助理(Personal Agent)这个词已经喊了两年多,但真正能落到工程实处的开源项目少得可怜。大多数时候,你能看到的只是一堆概念拼盘:说记忆、说规划、…

2026/10/13 10:21:51 阅读更多 →
K8s 探针配置的 3 个反直觉行为:为什么 Pod 一直没就绪

K8s 探针配置的 3 个反直觉行为:为什么 Pod 一直没就绪

我不会起名字322 后端 / 算法 / 数据库 📘 技术栈 | 🧩 力扣 Hot100 | 🐹 Go 项目 | 🟥 Redis | 🐬 MySQL 文章目录K8s 探针配置的 3 个反直觉行为:为什么 P…

2026/10/13 10:20:50 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →