
简介沪深两市所有股票自上市以来至2022年1月10日的完整日线行情在这份资源中一次汇集覆盖市场全貌主要面向量化研究者、技术分析爱好者及金融数据从业者可用于历史复盘、规律挖掘与策略开发。数据字段十分完整包含开盘价、收盘价、最高价、最低价、振幅、成交量、成交额、换手率并额外附带MACD、CCI以及同花顺多空指标等常用技术指标结果免去自行计算指标的工作便于直接用于趋势判断和买卖信号识别。整个压缩包约424.52MB内部仅含一个SQL文件可导入MySQL等关系型数据库通过标准SQL语句灵活完成日期筛选、个股对比、指标统计等操作既降低了数据获取门槛也便于二次加工。目前资源已有611人学习适合希望快速搭建股票历史数据仓库、进行交易策略回测或构建预测模型的读者导入后即可围绕全市场日线数据开展多周期分析、指标验证与量化实验显著缩短数据准备周期提升研究分析效率。1. 沪深全量日线从上市首日到 2022-01-10到底有多大2022 年初有个朋友找我做全市场选股回测第一件事就是核对数据。他拉了某只老票的日线我这边同样的代码、同样的日期区间收盘价对不上差了几个百分点。查到最后他用的数据源是前复权我这边是不复权加因子两边都没错但放在同一个回测里就是灾难。这个标题看起来很直白——把沪深股票历史以来到 2022-01-10 的全部日线数据拿下来——但真正做起来你会发现全部这两个字才是最难的部分。沪深两市 A 股到 2022 年初大约四千七百只从各自上市首日算起全部日线合计是千万行级别按 CSV 落地不到 1GBSQLite 单文件就能装下。听起来不大难的是边界停牌、退市、新股上市、除权除息、成交量单位、接口断点任何一个没处理干净回测结果都是玄学。这篇文章按我自己搭这套数据的顺序讲适合要跑历史回测、做全市场因子研究、或者想彻底摆脱每次现抓数据的人。2. 数据源选型与拉取管道把几千只股票的历史日线一次取全2.1 股票清单与交易日历轮询的前提是先定边界要做全量第一步不是写爬虫而是先拿到全量的清单。很多人在这一步就翻车——拿了一份当前正在交易的股票列表去拉历史结果 2022-01-10 之前已经退市的股票全部缺席回测结果自然偏向幸存者。我一般会把清单定义为沪深交易所曾经上市过的所有 A 股包含正常上市、ST、暂停上市、退市整理、以及已经退市的股票。清单字段这样设计字段类型说明symbolTEXT证券代码统一带交易所后缀nameTEXT证券简称随时可更新不参与主键exchangeTEXTSH / SZlist_dateTEXT上市日期ISO 格式delist_dateTEXT退市日期未退市则为空statusTEXT当前状态退市股同样保留这里的关键点是start_date 必须取 list_date而不是取一个统一的起始日期。老股票可能 1991 年就上市新股可能 2021 年才上市统一从 2010 年开始拉会让老股票缺历史、新股白跑空。end_date 取 min(2022-01-10, delist_date)退市股只拉到退市前最后一个交易日。交易日历也要单独建表。沪深交易所每年大约 242 到 244 个交易日2022-01-10 是 2022 年 1 月的正常交易日必须落在范围内。不要自己写节假日规则去推算元旦、春节、国庆的调休每年都不一样直接拿交易所口径的交易日历最稳后续做全市场切片、停牌检测都靠它。2.2 最小可用拉取脚本单只股票全历史日线拿到清单和日历之后先写一个针对单只股票的函数跑通一只再谈并发。我一般用公开行情接口返回 JSON 数组字段映射成统一的 DataFrame 再入库。下面是核心请求函数import requests import time import pandas as pd # 占位替换为你实际选用的公开数据源地址 ENDPOINT https://your-data-provider.example/api/daily def fetch_daily(symbol, start, end, retries3, timeout10): params { symbol: symbol, start_date: start, end_date: end, } for attempt in range(retries): try: r requests.get(ENDPOINT, paramsparams, timeouttimeout) r.raise_for_status() payload r.json() df pd.DataFrame(payload[items], columnspayload[columns]) df[symbol] symbol return df except Exception as e: if attempt retries - 1: raise wait 2 ** attempt # 指数退避1s, 2s, 4s print(f{symbol} 第 {attempt 1} 次失败{e}等待 {wait}s) time.sleep(wait)这个函数的参数有几个可以调retries 控制重试次数我一般设 3timeout 设 10 秒如果接口慢再往上加到 20wait 用指数退避而不是固定间隔避免接口限频后立刻重试继续触发风控。调用的时候传入单只股票的上市日至 2022-01-10df fetch_daily(000001.SZ, 1991-04-03, 2022-01-10) print(df.shape)代码里的 000001.SZ 只是格式示例真实清单里每只股票都按这个格式走。注意接口可能对单次查询的日期跨度有限制比如最长返回 2000 条如果一只股票历史超过这个长度就把区间切成多段再拼接。一个通用做法是先调用一次不带日期的接口拿总条数超过上限就按年份分段拉取最后 concat 去重。2.3 断点续采与并发控制别让全量任务死在第二天早上几千只股票单线程一轮可能要一两个小时中间网络抖动一次全部白跑。所以我从最开始就加了断点续采每完成一只股票就把它的 symbol 追加到 done.txt下次启动先读这个文件已经完成的任务直接跳过。并发也要控制。不要无脑开几十个线程大多数公开接口都有 QPS 限制并发一高就是批量拒答。我一般把并发压在 4 到 8 之间配合每只股票请求之间的 sleep既能跑完又不至于被封。import os from concurrent.futures import ThreadPoolExecutor, as_completed DONE_FILE done.txt def load_done(): if not os.path.exists(DONE_FILE): return set() with open(DONE_FILE, r, encodingutf-8) as f: return {line.strip() for line in f if line.strip()} def worker(symbol, list_date, end_date2022-01-10): if symbol in load_done(): return symbol, skip try: df fetch_daily(symbol, list_date, end_date) df.to_csv(fdaily/{symbol}.csv, indexFalse) with open(DONE_FILE, a, encodingutf-8) as f: f.write(symbol \n) return symbol, ok except Exception as e: return symbol, ferr:{e} with ThreadPoolExecutor(max_workers4) as pool: futures { pool.submit(worker, row[symbol], row[list_date]): row[symbol] for _, row in stock_list.iterrows() } for fut in as_completed(futures): symbol, status fut.result() print(symbol, status)这里 max_workers4 是经验值。如果你确认接口 QPS 有 20可以上到 8如果 QPS 只有 2那单线程都偏快就要在 fetch_daily 里加 sleep。断点文件按行追加只加不删重跑时通过 load_done 过滤已完成任务。失败的任务会返回 err 状态但不会中断整体调度日志里能看到是哪几只出了问题跑完再单独补这几只。3. 日线数据入库表结构、批量写入与去重策略3.1 为什么不用 CSV 直接当结果从文件到数据库很多人拉到 CSV 就停了这也行但只适合单只股票研究。全市场因子分析经常要按日期切片——比如2022-01-10 全市场所有股票的收盘价如果数据是一千多个 CSV 文件你得遍历全部文件才能拼出这一天的横截面跑一次就是一次全量扫描毫无效率。我一般会保留 CSV 作为原始备份但主查询放到 SQLite 里。SQLite 是单文件、零运维千万行数据完全扛得住索引建好之后单日切片毫秒级返回。等以后数据量继续涨、并发查询变多再迁到 PostgreSQL 或 ClickHouse表结构不变迁移成本很低。存储方式按日切片按股票关联数据量级运维成本CSV 分文件遍历所有文件麻烦1GB 级可接受零但要自己维护文件SQLite索引快查方便千万行友好零单文件MySQL/PG快方便亿行级需要部署3.2 表结构与写入代码SQLite 与 MySQL 两种落地我习惯先把库表结构定义清楚再写采集脚本避免后期反复迁移。核心表是三张stock_basic 放股票清单trade_cal 放交易日历daily_bar 放日线行情。复权因子单独建表见 3.3。PRAGMA journal_modeWAL; CREATE TABLE IF NOT EXISTS stock_basic ( symbol TEXT PRIMARY KEY, exchange TEXT NOT NULL, name TEXT, list_date TEXT NOT NULL, delist_date TEXT, status TEXT ); CREATE TABLE IF NOT EXISTS trade_cal ( trade_date TEXT PRIMARY KEY, is_trading INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS daily_bar ( symbol TEXT NOT NULL, trade_date TEXT NOT NULL, open REAL, high REAL, low REAL, close REAL, volume INTEGER, amount REAL, PRIMARY KEY (symbol, trade_date) ) WITHOUT ROWID; CREATE INDEX IF NOT EXISTS idx_daily_date ON daily_bar(trade_date);daily_bar 的主键用 (symbol, trade_date) 天然去重后面重复写入不会产生脏数据。WHTHOUT ROWID 在复合主键下能减少存储占用SQLite 对主键即行标识的场景很友好。volume 统一为股amount 统一为元这个约定必须在所有数据源接入时做强制转换否则后面会出大问题见 4.4。trade_date 用 TEXT 存 ISO 格式 YYYY-MM-DD可读性好字符串比较也等价于日期比较。写入用 executemany 批量提交不要一条一条 insert。我一般每 5000 行提交一次事务SQLite 在这种批量模式下表现很好import sqlite3 conn sqlite3.connect(market.db) cur conn.cursor() def upsert_daily(df, batch_size5000): rows [ ( r[symbol], r[trade_date], r[open], r[high], r[low], r[close], int(r[volume]), float(r[amount]), ) for _, r in df.iterrows() ] for i in range(0, len(rows), batch_size): batch rows[i:i batch_size] cur.executemany( INSERT OR REPLACE INTO daily_bar VALUES (?,?,?,?,?,?,?,?), batch, ) conn.commit()INSERT OR REPLACE 的语义就是幂等同一只股票同一天的记录重复写入只会覆盖不会累积。如果以后要保留哪个版本更可信的审计逻辑可以把 REPLACE 改成带 version 字段的排除式写入但对于个人研究库REPLACE 足够。3.3 复权因子单独建表回测需要的是后复权这是整套数据里最容易埋雷的地方。很多人直接把数据源返回的前复权价格存入数据库短期看很舒服长期看是灾难——前复权价格是拿最新复权基准反推出来的数据源哪天更新了分红送转历史价格整体平移你的回测结果跟着变。我建议的落地方式是daily_bar 只存不复权的原始 OHLC另外建一张复权因子表CREATE TABLE IF NOT EXISTS adj_factor ( symbol TEXT NOT NULL, trade_date TEXT NOT NULL, factor REAL NOT NULL, PRIMARY KEY (symbol, trade_date) ) WITHOUT ROWID;factor 是累计复权因子。后复权价的计算是 close * factor复权因子表本身不会因为时间推移而改变所以后复权价永远可复现。前复权价可以随时算出来close * factor / factor_latest其中 factor_latest 是每只股票因子表里最新一条。换句话说入库只存静态事实展示层再算动态价格。# 用 SQL 查询后复权收盘价 query SELECT d.trade_date, d.close, f.factor, d.close * f.factor AS close_adj FROM daily_bar d JOIN adj_factor f ON d.symbol f.symbol AND d.trade_date f.trade_date WHERE d.symbol ? ORDER BY d.trade_date 有一点必须提醒不同数据源的复权因子口径可能相反。有的源 factor 越大表示越早的价格被缩小有的则相反。入库前先拿一只知名老股票在除权日的价格验证一遍确认公式方向后再批量入库别拿全市场数据验证一个错误假设。4. 避坑排查全量日线数据采集的 6 个典型坑4.1 停牌日缺失是空行还是跳空现象查某只股票某一周的数据连续四五天没有记录一开始以为是拉漏了重新调接口还是没有。原因停牌期间没有成交接口自然不会返回 K 线。这个缺失是合法缺失不是采集失败。解决入库时不填 0、不填充前值。0 会被当成收盘价为 0 的极端行情前值填充会制造根本不存在的连续成交。要区分停牌缺失和拉取失败缺失用交易日历左连接查SELECT c.trade_date FROM trade_cal c LEFT JOIN daily_bar d ON c.trade_date d.trade_date AND d.symbol 000001.SZ WHERE c.trade_date BETWEEN 2021-01-01 AND 2022-01-10 AND c.is_trading 1 AND d.trade_date IS NULL;这条 SQL 能列出某只股票在指定区间内所有应当有记录但库里没有的交易日。对全市场跑一遍剩余的空缺才是真正需要补拉的。4.2 除权除息日跳空不复权数据看着像崩盘现象某只股票前一天收盘 20 元后一天直接变 12 元日收益率 -40%策略信号在这一天频繁误报。原因10 送 10 之类的除权除息股价按比例调整并不是真的跌了 40%。解决用复权因子还原真实收益。daily_bar 存原始价adj_factor 存因子计算收益时用 close_adj。我见过最血的教训是直接用不复权价算技术指标然后在除权日附近搞出一堆假金叉假死叉。记住一句原始价入库复权价计算因子单独存。任何策略计算都走复权口径。4.3 新股与退市股日期范围不是你想的那样现象统一按 2010-01-01 到 2022-01-10 拉取结果某些新股只有 2021 年后的数据某只 2015 年退市的股票完全查不到。原因新股在 2010 年还没有上市退市股在 2022 年早就不交易了接口按上市状态过滤后默认不返回退市股。解决清单里保留退市股拉取区间按 list_date 和 delist_date 收窄请求参数里明确带上上市状态退市。退市整理期股票的涨跌幅规则跟正常股票不同价格波动极大别拿常规的 10% 阈值去校验它们。处理完这批数据后退市股的历史 K 线也进了库幸存者偏差才能避免。4.4 成交量单位不一致手、股、万元三种都有现象跟另一个数据源对账同一只股票同一天的 volume 差 100 倍amount 差 10000 倍。原因不同数据源对 volume 的约定不一样有的返回手1 手 100 股有的返回股amount 有的返回元有的返回万元。解决入库前强制统一。我在写入函数里加一段断言利用财务恒等式单日成交金额约等于收盘价乘以成交量。注意这个等式只在成交量股、成交金额元时成立# 单位自检金额 ≈ 收盘价 × 成交量股 expected df[close] * df[volume] ratio (df[amount] - expected).abs() / (expected 1e-9) bad df[ratio 0.05] if not bad.empty: print(单位疑似不一致) print(bad[[symbol, trade_date, close, volume, amount]].head()) raise ValueError(请检查成交量/成交金额单位约定)0.05 的容差覆盖盘后数据微调。如果大量行都不过先查源头单位别急着改代码。4.5 接口断点与限频全量任务最怕半路翻车现象全量任务跑到第 3000 只网络抖动脚本退出重新运行又从第 1 只开始。原因没做断点持久化单只失败直接抛异常中断整个循环。解决已完成的 symbol 实时追加到 done.txt重跑自动跳过单只失败只记录错误不中断整体重试用指数退避限速用固定 sleep。启动方式用 nohup 跑后台日志单独输出nohup python fetch_all.py fetch.log 21 跑完看 fetch.log 里的 err 列表单独补拉这几只。补拉完成后建议再跑一次全量正常情况下全部返回 skip如果还有 ok说明上次漏了文件。4.6 全量核对股票数、交易日数、总行数三层校验现象任务跑完自我感觉很完整三个月后写策略才发现少了 2015 年某几个月的全市场数据。原因拉取任务从没做过完整性校验感觉拉完不可靠。解决入库后用 SQL 做三层核对每一层都有明确的数字预期校验项口径预期量级股票数SELECT COUNT(*) FROM stock_basic沪深 A 股数量 已退市数量几千只交易日数SELECT COUNT(*) FROM trade_cal WHERE trade_date 2022-01-101990 年到 2022 年 1 月约七千多个总行数SELECT COUNT(*) FROM daily_bar WHERE trade_date 2022-01-10千万行级别切分点行数SELECT COUNT(*) FROM daily_bar WHERE trade_date 2022-01-10接近当天正常交易股票数四千七百上下切分点行数是验证 2022-01-10 边界最直接的方法。如果这个数字明显偏少先查当天是否大面积停牌或接口漏拉。数字偏多查是否把非 A 股品种混了进来。四层校验全部通过这套历史数据才真正可以作为策略研究的底仓。5. 增量更新与数据质量自检让这套日线数据一直能用历史数据拿到手只是开始。只要还在做研究数据就得持续更新。我建议把 2022-01-10 当作历史切分点之后的每个交易日跑一次增量任务拉当天行情用 INSERT OR REPLACE 覆盖写入保证任务重复跑也不会产生重复记录。def incremental_update(target_date): # target_date 为最近一个交易日如 2022-01-11、2022-01-12 df fetch_all_daily(datetarget_date) upsert_daily(df) print(f{target_date} 更新完成行数 {len(df)})每天收盘后拉当日数据时我一般会连最近 5 个交易日一起重新覆盖。原因是数据源偶尔会修正之前几天的数据比如某些大单分类调整只拉当天会把修正漏掉。这个策略对全量历史数据也适用每周跑一次最近 20 个交易日覆盖把数据源的历史修正同步进库。数据质量自检我固定放在每周一跑。重点做三件事检查 close、volume、amount 的恒等关系是否被破坏检查单只股票超过 20% 的日涨跌幅记录剔除创业板、科创板、北交所、退市整理期后剩余异常剧目人工确认检查 2022-01-10 前后各 10 天是否出现大面积缺失。-- 周度检查找出异常的放量样本成交量超过过去20日均量10倍 SELECT d.symbol, d.trade_date, d.volume, avg20.avg_volume FROM daily_bar d JOIN ( SELECT symbol, AVG(volume) AS avg_volume FROM daily_bar WHERE trade_date 2022-01-10 AND trade_date DATE(2022-01-10, -20 days) GROUP BY symbol ) avg20 ON d.symbol avg20.symbol WHERE d.trade_date 2022-01-10 AND d.volume avg20.avg_volume * 10;早年做这套数据时我偷懒直接把前复权价格落库某个数据源更新了复权基准导致我的历史回测全线漂移白调了一周的参数。从那以后我的入库原则就一句话库里只存不复权原价和复权因子任何复权价格都靠查询时计算。这个习惯帮我省掉了后面无数次的为什么今天跑出来的结果跟昨天不一样。历史数据这碗饭慢工出细活希望这套流程能帮你少走点弯路也让你自己的数据底仓真正靠得住。本文还有配套的精品资源点击获取