简介:本资源是一套基于Baostock金融数据接口的股票与指数K线数据自动化下载与本地存储工具,面向量化研究者、金融分析师及入门级Python用户,解决高频次、多标的、多周期历史行情数据批量获取与结构化落盘的痛点。压缩包共4个文件(36KB),含核心脚本main.py(实现指数与全A股日线/5分钟K线自动抓取与CSV存储)、README.md(环境配置与调用说明)、说明文件.txt(字段定义与使用提示)及附赠资源.docx(常见问题与参数详解),类型覆盖代码、文档与实操指引。目前已有145人学习下载。用户可直接运行脚本获取上证指数、深证成指、沪深300、创业板指等主流指数及全部A股的历史日K线与5分钟K线数据,所有数据按代码+日期自动归档,支持断点续采与路径自定义,无需手动清洗即可用于回测、因子计算或可视化分析。
1. 为什么你手动导出K线数据的Excel总在第二天就失效?——用Baostock把上证指数、深证成指、沪深300、创业板指和全A股日线+5分钟线“钉死”在本地硬盘里
你是不是也经历过:凌晨三点爬通达信导出沪深300近五年日K,结果发现导出文件缺了2022年4月18日(那天交易所系统升级);或者用某财经网站API批量抓A股代码,跑了一夜,最后发现接口返回的“成交量”单位是“手”还是“股”根本没文档说明,导致回测信号全错;更别提那些标着“免费”的股票接口,调用10次后突然返回{"code":403,"msg":"请求过于频繁"}——连错误码都懒得写全。这不是玄学,是数据链路断裂的典型症状。本工具不碰任何第三方网页渲染、不依赖浏览器自动化(拒绝模拟鼠标点击这类不可靠操作),而是直连Baostock——一个由国内开发者维护、持续适配中登公司与交易所数据结构的Python金融数据接口。它能稳定获取上证指数(000001.SH)、深证成指(399001.SZ)、沪深300(000300.SH)、创业板指(399006.SZ)等核心指数,以及全量A股(含北交所、科创板、创业板)的日K线与5分钟K线,原始字段完整(开盘价、最高价、最低价、收盘价、成交量、成交额、换手率、振幅、涨跌幅),且全部落盘为本地SQLite数据库+CSV双备份。适合量化研究员做因子回测、实盘策略工程师搭本地行情服务、高校金融实验室建教学数据集——只要你的场景需要可复现、可审计、可离线验证的K线底仓,这个方案就是当前最轻量、最可控的落地路径。
2. 从零构建Baostock数据管道:安装、登录、校验三步闭环
Baostock不是黑匣子,它的稳定性来自对交易所原始数据包的解析逻辑,而非代理转发。这意味着你必须理解它的连接机制和认证边界——否则下载中途断连,连重试点都找不到。
2.1 安装与环境隔离:为什么必须用Python 3.8~3.11?
Baostock官方PyPI包(baostock)底层依赖pywin32(Windows)或pysqlite3(Linux/macOS)进行本地数据库写入,而这些依赖在Python 3.12+中存在ABI兼容性问题。我实测过3.12.3下bs.login()成功但bs.query_history_k_data_plus()返回空DataFrame,查日志才发现是SQLite WAL模式写入失败。
提示:生产环境务必用conda或venv隔离环境,避免全局Python污染。以下命令创建专用环境:
conda create -n bs_env python=3.10 conda activate bs_env pip install baostock pandas numpy sqlalchemy2.2 登录与会话保活:不要跳过bs.login()的返回值校验
Baostock采用长连接会话机制,每次bs.login()会返回一个包含error_code和error_msg的字典。常见误区是只写bs.login()不检查返回值,结果后续所有查询都静默失败。正确做法是:
import baostock as bs import sys lg = bs.login() if lg.error_code != '0': print(f"Baostock登录失败:{lg.error_msg}") sys.exit(1) print(f"Baostock登录成功,版本:{lg.version}")注意:bs.login()默认使用匿名账号(无需注册),但每IP每日限调用1000次。若需更高频次,需在Baostock官网申请正式账号并传入user_id和password参数。
2.3 数据源校验:用指数代码验证接口可用性
别急着下载全A股,先用上证指数(sh.000001)和深证成指(sz.399001)做探针测试。这两个代码永不变更,且数据连续性最强:
rs = bs.query_history_k_data_plus( "sh.000001", "date,open,high,low,close,volume,amount,turn,tradestatus,pctChg,isST", start_date="2023-01-01", end_date="2023-01-05", frequency="d", adjustflag="3" ) print(f"上证指数数据条数:{rs.data.__len__()}") # 输出应为5(工作日),若为0则说明网络/认证/日期范围任一环节异常关键参数说明:
frequency="d":日线;"5":5分钟线(注意:5分钟线仅提供最近1年数据)adjustflag="3":复权方式,1=不复权,2=前复权,3=后复权(推荐,避免除权缺口)tradestatus字段用于过滤停牌日(值为1表示交易,0表示停牌),这是清洗数据的第一道闸门
3. 全量A股+四大指数的自动化下载:分片调度与断点续传设计
全A股约5000只标的,若逐个循环调用query_history_k_data_plus(),单进程耗时超8小时且极易因网络抖动中断。必须引入分片、并发、状态记录三重机制。
3.1 股票池动态生成:绕过静态CSV列表的硬编码陷阱
很多教程让你先下载一个“所有A股代码表”,但这个表本身就有坑:
- 某些代码已退市但未从列表剔除(如
000024.SZ,2023年已终止上市) - 新上市股票(如2024年新上市的
688680.SH)不会出现在旧列表中 - 北交所股票(
8开头)和科创板(688开头)在部分老列表中缺失
正确做法是实时查询:
# 获取全量A股代码(含北交所、科创板、创业板) rs = bs.query_all_stock(day="2024-06-01") # 指定任意交易日即可 stocks = [] while (rs.error_code == '0') & rs.next(): stock = rs.get_row_data() # 过滤掉B股(.SB)、基金(.OF)、债券(.IB)等非股票标的 if stock[0].endswith(('.SH', '.SZ')) and not stock[0].startswith(('2', '9')): stocks.append(stock[0]) print(f"实时获取A股代码总数:{len(stocks)}") # 通常为5000±50注意:query_all_stock()返回的是当日上市状态,自动排除已退市代码,且包含当日新上市股票。
3.2 分片下载策略:按代码首字母分组+时间窗口滑动
将5000只股票按代码前缀分10组(如000*,001*,002*...688*,8*),每组内再按交易日分段(如2010-2015、2016-2020、2021-2024)。这样既避免单次请求超时,又便于失败后定位:
from concurrent.futures import ThreadPoolExecutor, as_completed import time def download_stock_kline(code, start_date, end_date, freq): """单只股票下载函数,带重试与异常捕获""" for attempt in range(3): # 最多重试3次 try: rs = bs.query_history_k_data_plus( code, "date,open,high,low,close,volume,amount,turn,tradestatus,pctChg,isST", start_date=start_date, end_date=end_date, frequency=freq, adjustflag="3" ) if rs.error_code == '0': df = rs.get_data() # 过滤停牌日 df = df[df['tradestatus'] == '1'] return df except Exception as e: time.sleep(1) return None # 示例:下载上证指数日线(独立于A股池,单独处理) index_codes = ['sh.000001', 'sz.399001', 'sh.000300', 'sz.399006'] with ThreadPoolExecutor(max_workers=4) as executor: futures = { executor.submit(download_stock_kline, code, '2010-01-01', '2024-06-01', 'd'): code for code in index_codes } for future in as_completed(futures): code = futures[future] df = future.result() if df is not None and not df.empty: df.to_csv(f"data/{code}_daily.csv", index=False) print(f"{code} 日线下载完成,共{len(df)}条")3.3 断点续传状态机:用SQLite记录每只股票的最后成功日期
单纯靠try-except无法解决网络中断后的续传问题。必须建立状态表:
CREATE TABLE download_status ( code TEXT PRIMARY KEY, last_success_date TEXT, last_update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, status TEXT CHECK(status IN ('success', 'failed', 'pending')) );下载主循环逻辑:
def get_next_download_range(code): """根据状态表返回下次需下载的日期范围""" conn = sqlite3.connect("data/status.db") cursor = conn.cursor() cursor.execute("SELECT last_success_date FROM download_status WHERE code=?", (code,)) row = cursor.fetchone() if row and row[0]: start_date = (pd.to_datetime(row[0]) + pd.Timedelta(days=1)).strftime("%Y-%m-%d") else: start_date = "2010-01-01" end_date = datetime.now().strftime("%Y-%m-%d") conn.close() return start_date, end_date # 下载时更新状态 def update_status(code, date_str, status): conn = sqlite3.connect("data/status.db") cursor = conn.cursor() cursor.execute(""" INSERT OR REPLACE INTO download_status (code, last_success_date, status) VALUES (?, ?, ?) """, (code, date_str, status)) conn.commit() conn.close()这样即使程序崩溃,重启后自动从断点继续,而非重复下载或跳过。
4. 本地存储双保险:SQLite结构化存档 + CSV原始备份
只存CSV是自欺欺人——没有索引、无法关联查询、字段类型丢失;只存SQLite又难被Excel直接打开。必须双存,并设计合理的表结构。
4.1 SQLite表结构设计:为什么用code+date作联合主键?
CREATE TABLE kline_daily ( code TEXT NOT NULL, -- 股票代码,如 sh.000001 date TEXT NOT NULL, -- 交易日期,格式 YYYY-MM-DD open REAL, -- 开盘价 high REAL, -- 最高价 low REAL, -- 最低价 close REAL, -- 收盘价 volume INTEGER, -- 成交量(股) amount REAL, -- 成交额(元) turn REAL, -- 换手率(%) tradestatus INTEGER, -- 交易状态:1=交易,0=停牌 pctChg REAL, -- 涨跌幅(%) isST INTEGER, -- 是否ST:1=是,0=否 PRIMARY KEY (code, date) -- 关键!避免重复插入同一日期数据 );优势:
- 联合主键天然去重,防止同一股票同日数据多次写入
code字段支持快速筛选(如WHERE code LIKE 'sh.%'查所有沪市股票)date字段支持时间范围查询(如WHERE date BETWEEN '2023-01-01' AND '2023-12-31')
4.2 CSV备份规范:字段顺序与缺失值处理
CSV必须严格按Baostock原始字段顺序输出,且缺失值统一用NULL(非空字符串或nan):
# 保存CSV时强制类型转换 df['open'] = pd.to_numeric(df['open'], errors='coerce') df['volume'] = pd.to_numeric(df['volume'], errors='coerce') df.to_csv( f"data/csv/{code}_{freq}.csv", index=False, na_rep="NULL", # 关键!避免Excel误读为0 float_format="%.4f" # 价格保留4位小数 )注意:Baostock返回的
volume字段在某些历史数据中为字符串(如"1.23e+07"),直接存CSV会导致Excel科学计数法显示错误。必须用pd.to_numeric(..., errors='coerce')转为数值型。
4.3 索引优化:让回测查询提速10倍
对高频查询字段建索引:
CREATE INDEX idx_kline_code ON kline_daily(code); CREATE INDEX idx_kline_date ON kline_daily(date); CREATE INDEX idx_kline_code_date ON kline_daily(code, date); -- 复合索引,用于个股时间序列查询实测:查询某只股票2020-2024年全部数据,无索引耗时2.3秒,加索引后降至0.18秒。
5. 避坑指南:那些让下载任务半夜崩掉的5个血泪细节
5.1 现象:下载创业板指(399006.SZ)时pctChg字段全是NULL
原因:创业板指代码在Baostock中必须用sz.399006(深交所前缀),若误写为sh.399006(上交所前缀),接口虽不报错但返回空字段。
解决:所有指数代码严格按交易所前缀——上证系用sh.,深证系用sz.。可建映射表校验:
INDEX_MAP = { "000001.SH": "sh.000001", # 上证指数 "399001.SZ": "sz.399001", # 深证成指 "000300.SH": "sh.000300", # 沪深300 "399006.SZ": "sz.399006", # 创业板指 }5.2 现象:5分钟线下载到2023-06-01后突然中断,后续日期全为空
原因:Baostock的5分钟线数据仅保留最近365天,end_date设为未来日期(如2024-12-31)会导致整个请求返回空。
解决:动态计算end_date为datetime.now().strftime("%Y-%m-%d"),且start_date不能早于end_date - 365 days。
5.3 现象:多线程下载时CPU飙升至100%,但实际吞吐量反而下降
原因:Baostock底层socket连接有并发限制,max_workers>4会导致大量连接超时,线程阻塞等待。
解决:线程池max_workers固定设为4,配合time.sleep(0.1)在每次查询后微延时,实测吞吐量提升37%。
5.4 现象:SQLite写入时报错database is locked
原因:多个线程同时写同一数据库文件,未启用WAL模式。
解决:初始化数据库时执行:
conn.execute("PRAGMA journal_mode=WAL;") conn.execute("PRAGMA synchronous=NORMAL;")5.5 现象:turn(换手率)字段在2010年前数据中大量为0.0
原因:交易所早期未披露换手率,Baostock返回默认值0.0而非NULL,导致回测时误判流动性。
解决:对turn==0.0且volume>0的记录,标记为NULL并加注释字段source_note="pre_2012_turn_unavailable"。
6. 进阶技巧:用本地K线库做三件事——验证复权一致性、识别异常停牌日、生成行业板块快照
6.1 复权验证:为什么后复权价格比前复权更适合多周期策略?
后复权(adjustflag=3)将所有历史价格按最新股本折算,保证价格曲线连续无缺口。验证方法:
# 取同一只股票两段相邻日线,检查复权后收盘价是否连续 df = pd.read_sql("SELECT date,close FROM kline_daily WHERE code='sh.000001' ORDER BY date LIMIT 100", conn) df['date'] = pd.to_datetime(df['date']) df['diff'] = df['close'].diff() # 后复权下diff应接近0(除权日除外) # 找出diff绝对值>5%的日期,即为除权日 ex_dividend_days = df[abs(df['diff']/df['close'].shift(1)) > 0.05]['date'].dt.strftime("%Y-%m-%d").tolist()实操结论:后复权数据在MACD、均线类指标中表现更稳定,前复权更适合计算历史涨跌幅百分比。
6.2 异常停牌识别:用tradestatus和volume双重校验
仅靠tradestatus=0可能漏掉“假交易日”(如2020年新冠疫情期间部分股票公告停牌但系统未同步)。需叠加成交量阈值:
# 定义异常停牌:tradestatus=1但volume<1000(单位:股) abnormal_suspension = df[ (df['tradestatus'] == '1') & (pd.to_numeric(df['volume'], errors='coerce') < 1000) ] if not abnormal_suspension.empty: print(f"发现异常交易日:{abnormal_suspension['date'].tolist()}")这类日期需人工核查公告,避免策略误触发。
6.3 行业板块快照:用申万一级行业分类生成板块轮动信号
Baostock不提供行业分类,但可结合Tushare或聚宽的行业映射表(需自行维护)。关键技巧:
# 假设已有industry_map.csv:code,industry_name industry_df = pd.read_csv("data/industry_map.csv") # 计算每个行业的日均涨跌幅 sector_daily = df.merge(industry_df, on='code').groupby(['date','industry_name'])['pctChg'].mean().reset_index() # 生成板块强度排名(当日涨幅TOP3) top_sectors = sector_daily.groupby('date').apply( lambda x: x.nlargest(3, 'pctChg')[['industry_name','pctChg']] ).reset_index(drop=True)这样就能脱离第三方平台,用本地数据生成纯自主的行业轮动信号。
最后说个我踩过的坑:别在下载脚本里写time.sleep(1)来防限流——Baostock的限流是按IP+会话ID统计的,sleep治标不治本。真正可靠的做法是用状态表记录每只股票的最后下载时间,确保同一IP每秒不超过1次请求。现在我的生产环境已稳定运行14个月,全量日线更新延迟<2小时,5分钟线延迟<15分钟。这套方案不追求“全自动无人值守”,而是把每一个可能断裂的环节都变成可监控、可回滚、可审计的状态节点。数据管道的尊严,不在它多快,而在它多稳——希望帮到你。
本文还有配套的精品资源,点击获取