
简介这份数据集收录了沪深股市自开市以来至2022年1月10日的全部日线行情面向量化研究者、投资分析师与算法交易学习者用于趋势研判、技术指标计算与策略回测。数据字段覆盖开盘价、收盘价、最高价、最低价、振幅、成交量、成交额、换手率等基础行情并附带MACD、CCI以及同花顺手机版多空指标便于直接开展技术分析与信号验证。资源以单个SQL文件形式提供压缩包约424.52MB共1个文件导入关系数据库后即可用SQL语句完成提取、统计与复杂查询适合构建预测模型或批量回测。目前已有606人学习下载社区成员还可在评论区留言请求补充更多技术指标方便按需扩展分析维度。1. 沪深股票历史日线数据从数据获取到本地落库的完整路径做量化回测、因子挖掘或者行情复盘绕不开一份干净、完整、可复现的沪深股票历史日线数据。标题里说的“历史以来到 2022-01-10 的全部日线数据”本质是一份截面时间点明确、覆盖全市场、字段结构统一的日频行情快照。它要解决的核心问题是你手上得有一份能直接喂给 pandas、能按股票代码和交易日双索引查询、不会因为接口变动而中途断供的本地数据底座。适合谁做中低频策略回测的量化开发者、需要批量计算技术指标的研究者、以及想自己搭一套行情数据库的工程师。这份数据不是拿来“看行情”的是拿来“算东西”的所以字段完整性、复权处理、停牌标记这三件事比数据量本身更决定后续能不能用。2. 数据来源选型为什么本地落库比在线接口更靠谱2.1 三种常见数据获取路径的取舍做沪深日线数据从业者手里通常有三条路一是直接用开源财经数据接口在线拉取二是从数据服务商购买整理好的历史包三是自己从交易所公开信息或行情软件导出后清洗。三条路各有边界选错了后面全是返工。在线接口的优点是上手快几行代码就能拿到数据但它的致命问题是不稳定和限流。你回测跑到一半接口改了字段名或者加了频率限制整个流程就断了。而且很多免费接口对历史数据的回溯深度有限想拿到“历史以来”的全量数据往往要分多次请求再拼接中间一旦漏了某天你很难察觉。购买数据包的优点是省事、字段规范但成本高而且你拿到的复权方式、停牌处理逻辑是别人定的和你的回测框架未必对得上。自己清洗这条路最累但可控性最强适合对数据质量有洁癖、且愿意一次性投入建好底座的人。我一般会建议用开源接口做增量更新用一次性全量拉取做冷启动落库到本地 SQLite 或 Parquet。这样既避免了长期依赖在线服务又保留了后续按需补数据的灵活性。标题里“到 2022-01-10”这个截止日期很关键它意味着这是一份静态历史快照不需要考虑之后的增量同步适合做一次性建库。2.2 字段设计哪些列必须有哪些列可以后算一份能用的日线数据最小字段集是股票代码、交易日期、开盘价、最高价、最低价、收盘价、成交量、成交额。这八个字段缺一不可尤其是成交额很多技术指标和流动性因子都依赖它。但光有这些还不够。你必须额外处理三件事第一是复权。原始行情是不复权的遇到除权除息日价格会出现跳空直接拿来做收益率计算会严重失真。常见做法是同时保留不复权价和前复权价或者只保留前复权价但在字段名里标注清楚。前复权的逻辑是以最新价格为基准向前调整适合回测后复权是以上市首日为基准向后调整适合计算长期持有收益。标题截止到 2022-01-10如果你用前复权那基准日就是这一天。第二是停牌标记。停牌当天没有成交但很多数据源会直接跳过这一天导致你的时间序列出现空洞。正确做法是保留停牌日的行价格字段填前一交易日收盘价或空值并加一个is_suspended布尔列。这样你在计算滚动窗口指标时不会因为日期跳跃而算错。第三是股票状态。是否 ST、是否退市、上市日期、退市日期。这些信息决定了你的股票池怎么筛。比如回测时你要排除 ST 股或者只保留在某个时间点仍在交易的股票没有这些列你就得额外维护一张表。下面是一个推荐的表结构用 SQLite 建表CREATE TABLE daily_bar ( ts_code TEXT NOT NULL, -- 股票代码如 000001.SZ trade_date TEXT NOT NULL, -- 交易日期格式 YYYY-MM-DD open REAL, -- 开盘价前复权 high REAL, -- 最高价前复权 low REAL, -- 最低价前复权 close REAL, -- 收盘价前复权 vol REAL, -- 成交量手 amount REAL, -- 成交额千元 is_suspended INTEGER DEFAULT 0, -- 是否停牌0 否 1 是 is_st INTEGER DEFAULT 0, -- 是否 ST0 否 1 是 list_date TEXT, -- 上市日期 delist_date TEXT, -- 退市日期未退市为空 PRIMARY KEY (ts_code, trade_date) );建表时把ts_code和trade_date设成联合主键这样天然去重也方便按股票或按日期建索引。vol和amount的单位要统一不同数据源给的单位可能是股、手、元、千元落库前必须换算一致否则后面算换手率、成交额分布全错。注意字段单位不统一是新手最容易翻车的地方。成交量有的源给“股”有的给“手”1 手 100 股。成交额有的给“元”有的给“千元”。落库前一定写个校验脚本抽查几只股票的成交额除以成交量看均价是否落在当日最高最低价之间不在就说明单位错了。3. 全量拉取与本地落库可复现的代码路径3.1 用 Python 批量拉取并写入 SQLite假设你选定了某个开源财经数据接口具体用哪个按你手头能稳定访问的来核心逻辑是先拿到全市场股票列表再按股票逐只拉取日线最后批量写入 SQLite。下面是一段可复现的骨架代码重点看它的分批、重试、断点续传设计。import sqlite3 import time import pandas as pd from datetime import datetime # 假设你有一个 fetch_daily 函数传入代码和起止日期返回 DataFrame # 这里只写落库和调度逻辑 DB_PATH ashare_daily.db END_DATE 2022-01-10 def get_stock_list(): 获取全市场股票列表返回 ts_code 列表 # 实际实现依赖你的数据源这里用占位 # 常见做法是从数据源的股票列表接口拉取过滤掉退市和未上市的 pass def fetch_daily(ts_code, start_date, end_date): 拉取单只股票日线返回 DataFrame # 实际实现依赖你的数据源 pass def save_to_db(df, conn): 批量写入忽略重复 df.to_sql(daily_bar, conn, if_existsappend, indexFalse, methodmulti, chunksize500) def main(): conn sqlite3.connect(DB_PATH) stocks get_stock_list() total len(stocks) for i, ts_code in enumerate(stocks): try: # 断点续传先查库里这只股票已经有多少条 cur conn.execute( SELECT COUNT(*) FROM daily_bar WHERE ts_code ?, (ts_code,)) existing cur.fetchone()[0] if existing 0: # 简单策略已有数据就跳过适合一次性冷启动 # 更精细的做法是查最大日期从那天之后续拉 print(f[{i1}/{total}] {ts_code} 已有 {existing} 条跳过) continue df fetch_daily(ts_code, 1990-01-01, END_DATE) if df is None or df.empty: print(f[{i1}/{total}] {ts_code} 无数据) continue # 字段清洗和单位换算在这里做 df df.rename(columns{ trade_date: trade_date, open: open, high: high, low: low, close: close, vol: vol, amount: amount }) df[ts_code] ts_code df[trade_date] pd.to_datetime(df[trade_date]).dt.strftime(%Y-%m-%d) df[is_suspended] 0 df[is_st] 0 df[list_date] None df[delist_date] None save_to_db(df, conn) conn.commit() print(f[{i1}/{total}] {ts_code} 写入 {len(df)} 条) except Exception as e: print(f[{i1}/{total}] {ts_code} 失败: {e}) # 失败不中断继续下一只最后统一补 time.sleep(1) continue # 控制频率避免触发限流 time.sleep(0.3) conn.close() if __name__ __main__: main()这段代码的关键点不在拉取本身而在容错和续传。existing 0就跳过是最粗暴的续传策略适合一次性冷启动。如果你拉到一半断了重新跑一遍已经入库的股票会自动跳过不会重复写入。try/except包住单只股票的拉取一只失败不影响整体最后你可以根据日志把失败的股票单独补拉。chunksize500是写入批量大小太小会导致频繁 IO太大占内存。500 到 1000 之间比较稳。methodmulti让 pandas 用多值插入比逐行插入快一个数量级。3.2 拉完之后必须做的三项校验数据入库不等于能用。我一般会跑三个校验脚本任何一个不过整批数据都要打回重来。第一日期连续性校验。对每只股票检查它的交易日期序列是否和交易日历对齐。A 股交易日历可以从数据源单独拉一份或者用pandas_market_calendars这类库生成。如果某只股票在某个交易日没有记录要么是停牌要么是数据缺失。停牌可以接受缺失必须补。import pandas as pd def check_date_gap(conn, trade_calendar): 检查每只股票缺失的交易日 df pd.read_sql(SELECT ts_code, trade_date FROM daily_bar, conn) df[trade_date] pd.to_datetime(df[trade_date]) calendar pd.to_datetime(trade_calendar) results [] for ts_code, group in df.groupby(ts_code): dates set(group[trade_date]) # 只检查该股票上市期间的交易日 missing [d for d in calendar if d not in dates] if missing: results.append((ts_code, len(missing), missing[:5])) return results第二价格逻辑校验。检查low open high、low close high、low high。任何一条不满足说明数据源有问题或者复权计算错了。这个校验能抓出大部分脏数据。第三成交额与成交量匹配校验。计算amount / (vol * 100)得到均价假设 vol 单位是手amount 单位是元检查这个均价是否落在[low, high]区间内。如果大面积偏离说明单位换算错了。提示校验脚本不要只跑一遍就完事。每次增量更新后都要重跑尤其是价格逻辑校验它能帮你发现数据源悄悄改了复权算法的情况。4. 避坑与排查沪深日线数据落库的五个血泪教训4.1 现象回测收益率出现离谱的跳变某天单只股票涨了 300%原因除权除息日没有做复权处理或者复权因子用错了。很多数据源同时提供不复权价和复权价如果你拉的是不复权价但字段名没标注直接拿来算收益率除权日就会出现巨大跳空。解决落库时明确区分复权方式。我一般会建两张表一张daily_bar_raw存不复权原始数据一张daily_bar_adj存前复权数据。回测统一用daily_bar_adj需要看真实成交价时查daily_bar_raw。复权因子单独存一张表方便追溯。4.2 现象某只股票在某个时间段完全没有数据但该股票当时正常交易原因数据源对这只股票的代码有过变更比如从000001.SZ变成过别的后缀或者数据源在某个时间段漏拉了。另一种可能是该股票当时停牌但数据源直接跳过了停牌日而不是保留空行。解决先查交易日历确认那段时间是否真的停牌。如果没停牌去数据源核对代码是否变更。落库时强制保留停牌日价格填前一交易日收盘价is_suspended置 1。这样时间序列不会断。4.3 现象成交量单位混乱有的股票 vol 是几万有的是几百万原因不同数据源、甚至同一数据源的不同接口成交量单位可能不一致。有的给“股”有的给“手”。如果你从多个来源拼数据单位不统一几乎必然发生。解决落库前统一换算成“手”。写一个校验函数抽样计算amount / (vol * 100)如果结果不在当日价格区间内就尝试amount / vol看哪个匹配。匹配上的那个就是正确的单位关系。这个校验要针对每只股票单独做因为不同来源的股票可能混在一起。4.4 现象SQLite 写入速度越来越慢最后卡死原因没有建索引或者每次写入都开新事务。SQLite 默认每个 INSERT 都是一个事务逐条写入时磁盘 IO 会爆炸。另外如果表上没有主键或索引随着数据量增大插入时的去重检查会越来越慢。解决建表时设联合主键(ts_code, trade_date)写入时用executemany或 pandas 的to_sql批量提交chunksize设 500 到 1000。如果数据量超过千万行考虑换 Parquet 按年分区存储查询时用 DuckDB 或 Polars 读比 SQLite 快很多。4.5 现象拉取到一半接口返回空数据但股票明明在交易原因触发了数据源的频率限制或者当天是节假日但你的交易日历没更新。还有一种可能是数据源对历史数据的回溯深度有限比如只保留最近 5 年你拉 1990 年的数据自然为空。解决拉取前先确认数据源的历史回溯范围。如果确实有限换一个能提供全量历史的源或者从多个源拼接。频率限制方面在循环里加time.sleep单只股票之间间隔 0.3 到 0.5 秒批量拉取时不要开多线程猛冲。遇到空数据先记录日志不要直接跳过最后统一分析是接口问题还是数据本身没有。5. 从静态快照到可查询数据底座两个进阶技巧5.1 用 DuckDB 把 Parquet 变成秒级查询的行情库SQLite 适合中小规模数据但当你把全市场 5000 多只股票、30 年的日线数据全部落库后行数会到千万级甚至亿级。这时候用 SQLite 做聚合查询比如“计算所有股票在 2020 年的日均成交额”会明显变慢。我的做法是原始数据存 Parquet按年分区用 DuckDB 做查询引擎。Parquet 的列式存储对行情数据特别友好因为你经常只需要读close和vol两列不需要把整行拉出来。按年分区后查询某一年数据时 DuckDB 会自动裁剪分区只读那一个目录。import duckdb import pandas as pd # 假设数据已经按年存成 parquet路径如 data/2020.parquet con duckdb.connect() # 查询 2020 年所有股票的平均收盘价 result con.execute( SELECT ts_code, AVG(close) AS avg_close FROM read_parquet(data/2020.parquet) GROUP BY ts_code ORDER BY avg_close DESC LIMIT 10 ).fetchdf() print(result)DuckDB 直接读 Parquet 不需要建表、不需要导入查询速度比 SQLite 快一个数量级。如果你需要频繁按股票代码查询可以在 Parquet 之上再建一个 DuckDB 的持久化表加索引。5.2 用交易日历对齐做滚动窗口计算避免停牌导致的错位停牌是日线数据里最容易被忽视的坑。假设你在算 20 日移动平均如果某只股票停牌了 5 天你的数据里这 5 天是缺失的直接rolling(20)会把停牌前后的数据连在一起算结果完全错误。正确做法是先按交易日历 reindex停牌日价格填前一交易日收盘价成交量填 0然后再做滚动计算。这样窗口对齐的是真实交易日而不是数据行数。import pandas as pd def align_to_calendar(df, trade_calendar): 将单只股票数据对齐到交易日历 df df.set_index(trade_date).sort_index() calendar pd.DatetimeIndex(trade_calendar) # 只保留该股票上市期间的交易日 start df.index.min() end df.index.max() calendar calendar[(calendar start) (calendar end)] df df.reindex(calendar) # 停牌日价格前向填充成交量填 0 df[close] df[close].ffill() df[open] df[open].fillna(df[close]) df[high] df[high].fillna(df[close]) df[low] df[low].fillna(df[close]) df[vol] df[vol].fillna(0) df[amount] df[amount].fillna(0) df[is_suspended] df[is_suspended].fillna(1) return df.reset_index().rename(columns{index: trade_date})这个函数做完之后你的数据里每个交易日都有行停牌日的价格是前收成交量是 0。再算rolling(20).mean()时窗口覆盖的是真实的 20 个交易日不会因为停牌而错位。我自己的习惯是任何滚动窗口计算之前先跑一遍日历对齐把对齐后的数据存成单独的表。这样回测和因子计算都基于同一份对齐数据避免不同模块各自处理导致结果不一致。这份沪深日线数据从拉取到落库再到对齐整套流程跑通一次大概需要几个小时但建好之后后面所有的策略研究都在这份底座上做省下的重复清洗时间远超过一次性投入。希望帮到你。本文还有配套的精品资源点击获取