简介:沪深股票历史日线数据资源,覆盖沪深两市全部股票自上市日至2022年1月10日的每日行情。数据字段除开盘价、收盘价、最高价、最低价、振幅、成交量、成交额、换手率外,还预计算了MACD、CCI以及同花顺手机版多空指标,可直接用于判断价格趋势、识别超买超卖区域和辅助买卖决策,对技术分析爱好者、金融研究员及量化交易者都有实用价值。整包仅1个SQL文件(共1个文件),压缩后约424.52MB,利用SQL可灵活完成区间统计、指标筛选与回测数据提取,免去自行爬取清洗的繁琐步骤。已有605人学习下载,数据结构清晰,便于回溯不同市场周期下的个股表现,也可用于构建多因子模型或验证经典技术指标的有效性,适合需要全量历史A股数据做策略复盘或模型训练的开发者直接使用。
1. 沪深股票全量日线回补:从接口选型到数据校验的一次性到位
拿到“沪深股票历史以来到2022-01-10的全部日线数据”这个需求,第一反应不是写代码,而是先想清楚一件事:你要的是“能跑通的脚本”还是“能用于因子回测的可靠数据集”。这两者的差距非常大。2022年1月10日这个截止点意味着你需要从交易所最早的数据(上证1990年12月19日、深证1991年4月3日)一直拉到指定日期,跨越30多年、5000多只股票、每只股票几千条K线,总量在千万级记录。这个量级如果用公共接口逐日请求,按每秒3次的频率限制,纯串行下载需要跑十几个小时,而且大概率中途断掉。更麻烦的是,不同数据源对“历史以来”的定义不同,有的只给最近三年,有的复权因子缺失,有的停牌日直接留空洞。把这些问题拆开看,核心就三个:选对数据源、设计断点续传的回补流程、验证数据完整性和复权一致性。这篇文章把这套方案完整讲透。
2. 数据源选型与全量历史数据的获取机制
2.1 为什么免费接口里baostock最适合全量回补
市面上的A股日线数据接口看似很多,但能做全量历史回补的窗口其实很窄。你搜“沪深股票历史数据接口”,最先跳出来的是tushare、akshare、baostock三个。tushare的pro接口需要积分门槛,基础积分只能取最近几年的数据,历史全量需要较高积分或者付费,对于一次性回补需求不划算。akshare的底层多为爬虫实现,东方财富、新浪财经这些源经常改版,爬虫稳定性没法保证,而且历史数据最早只到2000年左右,再往前就断了。baostock是专门为量化研究设计的免费接口,不需要token,不限积分,数据从交易所成立日就开始覆盖,而且是批量返回——一次查询直接给一整只股票的全历史日线,不用自己按日期翻页。这就是为什么做全量回补时baostock是首选。
2.2 baostock的登录校验与数据工厂模式
baostock的使用方式比较特殊,它不是函数式调用,而是先登录拿到会话,再通过query_history_k_data_plus这个工厂方法取数据。登录动作会做本机校验和网络握手,返回的结果码0表示成功,10001001表示网络错误,10001002表示密码错误。虽然它是免费接口,但登录态是有时效的,长时间运行需要定期检查连接状态或者干脆每次批量任务重新登录。
import baostock as bs import pandas as pd from datetime import datetime # 登录获取会话 lg = bs.login() if lg.error_code != '0': print(f"登录失败: {lg.error_msg}") raise SystemExit(1) # 查询平安银行全历史日线 rs = bs.query_history_k_data_plus( "sz.000001", "date,code,open,high,low,close,preclose,volume,amount,adjustflag,turn,tradestatus", start_date='1991-04-03', end_date='2022-01-10', frequency="d", adjustflag="3" )这段代码里的adjustflag参数决定了复权行为,"3"表示不复权,"1"表示后复权,"2"表示前复权。全量回补时这里建议取"3"(不复权),因为后面做因子计算时需要自己控制前复权/后复权逻辑,依赖接口的复权结果容易踩坑。tradestatus字段是关键,1表示正常交易,0表示停牌,回补时不能只按日期切分,还得按交易状态过滤。
2.3 交易日历的坑:为什么不能按自然日顺序拉取
直接按股票代码遍历拉取是最直觉的做法,但这里有一个隐藏的效率问题。baostock的query_history_k_data_plus是按股票维度返回所有历史K线,不是按日维度。也就是说,每调用一次就拿回一整只股票的全量数据,循环5000次就能覆盖全市场。这个设计非常适合全量回补,但前提是你得先拿到完整的股票列表。常见的做法是用query_stock_basic拉取全部证券代码及状态,然后过滤出股票类型,再逐只回补。
# 获取全部证券代码 rs = bs.query_stock_basic() stock_list = [] while (rs.error_code == '0') & rs.next(): row = rs.get_row_data() stock_list.append({ 'code': row[0], # 证券代码 'code_name': row[1], # 证券名称 'type': row[3], # 1:股票 2:指数 3:其它 'status': row[4] # 1:上市 0:退市 }) df_stocks = pd.DataFrame(stock_list) # 只保留股票,同时包含已退市的,保证历史数据完整 df_stocks = df_stocks[df_stocks['type'] == '1'] print(f"股票总数: {len(df_stocks)}")这里一定要注意,status字段不要过滤掉退市股。沪深股票的历史全量数据里,退市股是必须包含的,否则做幸存者偏差研究时数据就是残缺的。baostock对退市股的历史数据仍然正常返回,只是最新的交易状态是退市。在拉取时你会发现,有些退市股的上市日期非常早,比如最早的一批“老八股”,它们的日线数据从1990年底就开始了。
2.4 增量字段的设计:一次回补,长期受益
全量回补不应该是用完一次就扔的脚本。2022年1月10日之后的每个交易日,你可能还需要增量追加数据。所以存储格式在一开始就要设计好。我建议用Parquet按年分区存储,而不是直接存CSV,原因有三个:一是千万级行数的CSV加载要几十秒,Parquet只要几秒;二是Parquet自带列式压缩,磁盘占用只有CSV的三分之一左右;三是后续做增量更新时可以直接用pyarrow的dataset API做分区写入,效率高很多。字段设计上,至少要保留date、code、open、high、low、close、volume、amount、adjustflag、turn、tradestatus这11个字段,其中adjustflag存当时取数用的复权标记,这是事后校验数据一致性的重要依据。
import pyarrow as pa import pyarrow.parquet as pq from pathlib import Path # 按年分区的写入逻辑 def save_daily_data(df: pd.DataFrame, base_dir: str = "./stock_data"): df['year'] = pd.to_datetime(df['date']).dt.year for year, group in df.groupby('year'): out_path = Path(base_dir) / f"year={year}" out_path.mkdir(parents=True, exist_ok=True) # 单只股票追加写入,使用行组追加模式 pq.write_to_dataset( group, root_path=base_dir, partition_cols=['year'], existing_data_behavior='overwrite_or_ignore' )overwrite_or_ignore这个参数很关键,它允许在已有的分区上追加新数据而不覆盖旧文件。不过要特别注意,这个参数是按文件粒度去重的,如果你的增量数据跨多个交易日后又回头去补前几天的数据,就会产生重复行。所以更稳妥的做法是把增量数据的日期范围严格控制在上一次回补的截止日之后。
3. 全量回补的完整流程:断点续传、频率控制与异常重试
3.1 基于已回补记录的状态表
全量回补几十万次接口调用,中途必然出问题:网络断、接口限流、进程被杀。不设计断点续传机制的回补脚本就是一次性脚本,重跑时要全部重新拉。正确的做法是先建一张回补状态表,记录每只股票的代码、最后成功回补的日期、失败次数和错误信息,每次启动时先检查这张表,跳过已经完成的,只重试失败的。这张状态表本身也可以和K线数据放在同一个Parquet目录下,用一个小型CSV文件维护就行。
# 初始化回补状态目录 mkdir -p ./stock_data/checkpoint touch ./stock_data/checkpoint/stock_progress.csv状态表的最小字段是:code、last_date、status、retry_count、last_error。last_date用于增量补数据时判断从哪天开始拉,status标记done、failed、partial三种状态,retry_count控制最大重试次数,last_error记录最近一次失败原因方便排查。
3.2 主循环:逐股回补与自适应限速
主循环的逻辑比想象中简单,但几个细节决定成败。首先是查询接口的start_date和end_date,全量回补时start_date直接填'1990-12-19'(上证最早交易日),end_date填'2022-01-10',不逐日请求,一次拉全。其次是频率控制,baostock对并发没有严格限制,但单进程连续高频请求会触发服务端的限流,表现为返回码为10002003(请求过于频繁)。最稳的做法是加入自适应限速:正常请求间隔0.1秒,如果遇到限流,指数退避到1秒、2秒、4秒,直到成功。
import time import random def fetch_stock_history(bs, code: str, start: str, end: str, max_retry: int = 5): retry_count = 0 wait_time = 0.1 # 正常间隔 while retry_count < max_retry: rs = bs.query_history_k_data_plus( code, "date,code,open,high,low,close,preclose,volume,amount,adjustflag,turn,tradestatus", start_date=start, end_date=end, frequency="d", adjustflag="3" ) if rs.error_code == '0': # 成功读取数据 rows = [] while rs.next(): rows.append(rs.get_row_data()) df = pd.DataFrame(rows, columns=rs.fields) # 只保留正常交易日 df = df[df['tradestatus'] == '1'] return df elif rs.error_code == '10002003': # 限流,指数退避 retry_count += 1 time.sleep(wait_time * (2 ** retry_count) + random.uniform(0, 0.5)) else: # 其他网络错误,等待后重试 retry_count += 1 time.sleep(2 + retry_count) return None这段代码的处理逻辑是:成功时一次性把所有行取干净,rs.next()循环要走到返回False为止,否则会导致后续查询结果串数据。tradestatus == '1'的过滤是为了剔除停牌日,停牌日接口返回的空值行如果不处理,后续计算收益时会当成0处理,造成严重误差。这里有个容易被忽略的细节:baostock的K线数据里也有tradestatus为0的行,这些行不是没有数据,而是当天停牌所以交易字段为空,如果你直接用dropna()清掉,就会误删整个日期行,导致时间轴断裂——虽然很低级但很多人第一次做都会踩。
3.3 全市场并行与进程级容错
单线程逐只回补到2022年1月10日对5000多只股票来说,假设单只股票返回平均120毫秒,大概需要10分钟。这个速度其实已经可以接受,不需要上多线程。但如果想跑得更快,可以用concurrent.futures做线程池,但要注意:baostock的session不是线程安全的,每个线程必须创建独立的连接。常见做法是用threading.local存线程专属的session,或者干脆用多进程,每个进程独立登录。
如果你选择多进程,我建议每500只股票一个子进程,进程内用ThreadPoolExecutor开4个线程,各线程独立baostock登录。整体速度可以压到2分钟左右。但代价是代码复杂度上升,调试难度增大。我先给一个稳妥的单线程方案,因为对多数一次性回补来说,10分钟的耗时是可以接受的。
import pandas as pd from pathlib import Path def backfill_all_stocks(df_stocks, checkpoint_file: str, end_date: str = '2022-01-10'): # 读取已有进度 if Path(checkpoint_file).exists(): df_ckpt = pd.read_csv(checkpoint_file, dtype={'code': str}) else: df_ckpt = pd.DataFrame(columns=['code', 'last_date', 'status', 'retry_count', 'last_error']) for _, row in df_stocks.iterrows(): code = row['code'] # 如果已经回补完成,跳过 existing = df_ckpt[df_ckpt['code'] == code] if not existing.empty and existing.iloc[0]['status'] == 'done': continue df = fetch_stock_history(bs, code, '1990-12-19', end_date) if df is not None and len(df) > 0: save_daily_data(df) # 更新进度 update_checkpoint(df_ckpt, code, df['date'].max(), 'done', 0, '') else: update_checkpoint(df_ckpt, code, '', 'failed', 1, 'query return empty')这里每次循环拿到的df是整个股票的历史,需要及时写入磁盘释放内存。你可能会问:一只股票30年的日线也就8000行左右,内存压力不大,但5000只全量回补如果一次性攒在内存里不写盘,最后阶段会占用几百MB甚至上GB。渐进写盘还有个好处是中断后已写入的数据立即可用,不必等全部跑完。
3.4 增量更新的时间边界
全量回补完成之后,2022-01-10之后的新交易日怎么追加?这是另一个常见需求场景:数据需要每天更新。增量更新的起点是每只股票在状态表里的last_date + 1个交易日,终点是今天。但注意:baostock的接口只接受YYYY-MM-DD字符串,不接受datetime对象,而且start_date必须是交易日,如果填了非交易日,接口不会报错,而是返回空结果。所以增量更新的标准做法是:先用query_trade_dates拿到最近的交易日历,再以last_date的下一个交易日作为起点。
def get_trade_dates_after(bs, last_date: str, end_date: str): rs = bs.query_trade_dates(start_date=last_date, end_date=end_date) dates = [] while (rs.error_code == '0') & rs.next(): row = rs.get_row_data() # 第3个字段是日历日期,第4个字段是是否交易日 dates.append(row[1]) return dates # 增量更新示例:找到last_date之后的下一个交易日 from datetime import datetime, timedelta last_date = '2022-01-07' next_date = datetime.strptime(last_date, '%Y-%m-%d') + timedelta(days=1) # 用query_trade_dates确认next_date是否为交易日,如果不是,顺延增量更新的频率控制可以更激进,因为每天只需要拉几千只股票当天的数据,接口压力小。但你如果用的是同一套查询函数,建议保留原来的限速策略,保证在极端情况下(比如一次性回补7天没跑)也能稳定完成。
4. 复权处理的三种选择与数据质量校验方法
4.1 前复权、后复权、不复权的适用场景
标题里的“全部日线数据”没有指定复权方式,但实际应用中你必须在取数时就决策,因为这直接影响后续策略表现。不复权数据是用来算真实成交额和成交量的,任何涉及到金额绝对值的统计(比如流动性分析)都只能用不复权数据。前复权数据适合技术分析,因为它保留了最近的价格绝对值,K线形态和均线系统看起来直观;但前复权数据的历史价格是动态变化的,每次新的除权除息事件发生,历史价格都会整体变动,所以在回测中反复加载前复权数据会导致价格不一致。后复权数据不存在这个问题,它把所有历史价格按复权因子统一调整,序列连续且唯一,适合做长期收益计算和因子回测。
4.2 用不复权数据手动计算复权因子的完整代码
因为baostock的adjustflag="3"是不复权数据,需要自己实现复权因子的计算。常见做法是用preclose(昨收)和close(今收)的关系来推算除权因子。这个逻辑很简单:正常情况下close / preclose接近当日收益率,但如果发生除权除息,这个比值会偏离正常波动范围,偏离的比例就是复权因子。注意,这个计算只对除权除息日之后的序列有效,前复权需要从最新日期往回推导,后复权需要从最早日期往未来推导。
import numpy as np def calc_adjust_factor(df: pd.DataFrame) -> pd.DataFrame: df = df.sort_values('date').reset_index(drop=True) # 理论收益率:当日收盘 / 昨收 df['ret_raw'] = df['close'] / df['preclose'] # 实际区间最大涨跌幅过滤(涨停/跌停之外的视为除权) mask = (df['ret_raw'] > 1.11) | (df['ret_raw'] < 0.89) df['adjust_factor_raw'] = np.where(mask, df['ret_raw'], 1.0) # 从最早日期累计后复权因子 df['adjust_factor_post'] = df['adjust_factor_raw'].cumprod() # 前复权因子 = 后复权因子 / 最新后复权因子 latest_factor = df['adjust_factor_post'].iloc[-1] df['adjust_factor_pre'] = df['adjust_factor_post'] / latest_factor # 生成复权价格 df['close_adj_pre'] = df['close'] * df['adjust_factor_pre'] df['close_adj_post'] = df['close'] * df['adjust_factor_post'] return df这段代码的处理精度对大多数场景够用,但有个明显的坑:正常涨停/跌停日的close/preclose也可能超过1.1或低于0.9,特别是创业板注册制之后20%的涨跌幅限制,会把正常的涨跌误判为除权。所以这个逻辑需要配合turn(换手率)或者成交量变化来二次过滤。一个简单的修正办法是:如果当日成交量没有异常放大,就不视为除权日。
4.3 数据完整性校验:用同源和异源数据交叉验证
全量回补完成后,数据质量校验是不可省的一步。校验分两个维度:一是完整性,检查每只股票的日线数量和时间连续性;二是一致性,用另一数据源抽取部分股票做收盘价对比,容差设为0.5%(四舍五入到小数点后两位导致的误差范围)。
# 检查每只股票是否存在跳空缺口(停牌日除外) def validate_continuous(df: pd.DataFrame, trade_dates: set) -> list: missing_dates = [] for date in df['date']: if date not in trade_dates: missing_dates.append(date) return missing_dates # 抽样与akshare对比 # import akshare as ak # df_ak = ak.stock_zh_a_hist(symbol="000001", period="daily", # start_date="20200101", end_date="20220110", adjust="") # 对比close列,允许0.5%误差对比时注意两个细节:一是抽样不能只看头部股票,要按代码分布随机抽50只,覆盖不同上市年份和不同行业;二是截止日期2022-01-10当天的数据在收盘前是无法取到的,所以校验样本范围要往前挪一天,从2022-01-07及之前取。如果发现某些股票在baostock和akshare中的收盘价不一致,优先检查该股票是否发生过送转或配股,因为两个数据源对复权因子的处理可能不同。
4.4 退市股与ST股的规范化处理
另一个容易被忽略的坑是ST和退市股。ST股有5%的涨跌幅限制,但这一信息不会直接出现在K线数据里,只能通过股票名称(code_name字段)判断。如果你在全量数据里看到某只股票的某一天涨幅超过5%,那不是数据错误,而是该股票在那一天还不叫ST,或者刚摘帽。真正的数据错误是:某只股票连续多日涨跌幅恒为0,但tradestatus显示为1,这通常是该股票长期停牌没有及时更新交易状态。处理方式是在校验逻辑里加一条:若同股票连续N天(比如30天)成交量为0但状态为正常,则标记为可疑数据,人工复核。
数据校验完成后,整个数据集就可以作为后续研究的基础了。不要只在本地存一份,建议同时导出一份CSV格式的备份放到另一块磁盘或对象存储上,因为Parquet虽然读取快,但如果你后续要用R、SPSS或者其他不支持Parquet的工具读取,就会面临格式转换的问题。CSV备份的代价是占用存储空间翻倍,但考虑到单份全量日线数据压缩后也就2GB左右,这个代价值得付。整个回补流程跑通后,你会得到一个完整的、时间连续的、字段齐全的沪深A股日线数据集,它可以支撑绝大多数量化研究和数据分析工作。
本文还有配套的精品资源,点击获取