
简介这是一套面向金融数据分析初学者与量化研究者的自动化数据获取工具专为解决A股及主流指数历史K线数据手动采集效率低、覆盖不全、存储分散等实际问题而设计。工具基于Baostock开源金融数据接口实现支持一键下载上证指数、深证成指、沪深300、创业板指等核心市场指数以及全部A股上市股票的日线与5分钟级别K线数据并自动完成本地结构化存储显著提升策略回测与时间序列分析的数据准备效率。压缩包共4个文件36KB含核心功能脚本main.py、使用说明README.md、基础配置说明txt及附赠资源docx文档类型分工明确Python脚本负责数据拉取与保存Markdown与文本文件提供环境配置、参数设置与字段释义Word文档补充常见问题与扩展用法。目前已有118人学习下载读者可直接运行脚本获取完整市场级历史行情数据无需额外开发即可接入后续分析流程。1. 项目缘起为什么我们需要一个本地的金融数据仓库在量化研究、策略回测甚至是日常的股票分析中数据是基石。很多朋友一开始会使用各种在线平台或财经网站手动下载CSV或者用一些封装好的库直接在线调用。这在小规模、低频次的分析中没问题但一旦你的研究进入深水区——比如需要回溯十几年的全市场日线数据做因子测试或者需要高频率的5分钟线来验证短线策略——问题就来了。首先是效率瓶颈。每次运行脚本都从网络请求数据受限于接口的调用频率、网络延迟获取全市场几千只股票多年的历史数据可能一跑就是一整天甚至因为超时或限制而中断。其次是分析灵活性。很多在线API对历史数据的查询有日期范围或数据量的限制你想做一次跨周期的复杂计算可能不得不拆分成多次请求代码变得臃肿。再者是稳定性和成本。免费的接口可能有调用上限或不稳定付费接口则是一笔持续的开销。更重要的是当你的分析逻辑固定后你希望每次回测都是在同一份“干净”的数据快照上进行避免因为源数据更新或接口变动导致结果不可复现。这就是我动手搭建这个本地化数据工具的核心动机将一次性的、耗时的网络数据获取转变为一次部署、永久可用的本地数据服务。我选择了Baostock这个免费、稳定的金融数据接口作为数据源。它提供了A股全市场的日K线、周K线、月K线以及5分钟、15分钟等分钟线数据数据质量经过社区验证足以支撑个人和中小团队的量化研究需求。这个工具的目标很明确自动化、批量化地从Baostock下载指定的股票和指数如上证指数、沪深300等的K线数据并以结构化的方式如按股票代码分目录存储为CSV文件保存到本地。之后所有的数据分析、回测都可以直接读取本地文件速度是毫秒级的完全摆脱网络依赖。下面我就把整个实现思路、关键代码、踩过的坑以及如何高效使用这份本地数据的经验毫无保留地分享出来。2. 核心工具选型为什么是Baostock Python在众多金融数据接口中我最终锁定了Baostock并在Python生态中构建了整个工具链。这个选择是经过一番对比和实际测试的。2.1 Baostock接口的独特优势Baostockwww.baostock.com是一个免费、开源的证券数据平台。对于我们的需求它的优点非常突出数据全面且免费涵盖A股所有上市股票的日、周、月K线以及5、15、30、60分钟线。还包括上证指数、深证成指、沪深300、创业板指等核心指数数据。这对于构建市场基准和进行板块分析至关重要。API简单清晰其Python库baostock的API设计非常直观。主要用到的就是login()、query_history_k_data()、logout()以及query_all_stock()这几个函数学习成本极低。稳定性与社区支持作为一个存在多年的项目其服务相对稳定。虽然没有官方的SLA保证但社区活跃遇到问题比较容易找到解决方案或替代思路。无复杂鉴权只需要简单的登录login即可获取数据避免了像一些商业API那样需要管理复杂的Token、签名等。当然它也有局限比如数据更新速度可能略慢于交易所通常T1但对于历史回测和研究而言这完全可接受。另外虽然免费但为了避免给服务器带来压力合理控制请求频率是每个使用者的基本素养。2.2 Python作为实现语言的理由Python几乎是量化金融领域的“普通话”。选择它是因为其强大的生态baostock库官方提供的Python SDK完美对接。pandas数据处理的利器。Baostock返回的数据可以直接转换为pandas DataFrame方便进行清洗、转换和存储。sqlite3/pandasI/O可以轻松地将数据存储到SQLite数据库或CSV文件中。我最初选择了CSV因为其简单、直观且能被几乎所有数据分析工具读取。并发控制使用concurrent.futures或asyncio可以轻松实现多线程/协程大幅提升批量下载数百只股票数据时的效率。整个技术栈轻量、高效且完全免费非常适合个人开发者和小型团队。3. 工具设计与实现从单次请求到批量自动化工具的核心逻辑是一个“生产者-消费者”模型先获取股票列表生产者然后遍历列表并发地请求每只股票的历史数据并保存到本地消费者。下面我分步骤拆解。3.1 环境准备与基础配置首先安装必要的库pip install baostock pandas然后规划本地存储目录。一个清晰的结构能让你日后管理数据事半功倍。我建议的目录结构如下financial_data_local/ ├── daily_k_data/ │ ├── sh.000001.csv # 上证指数 │ ├── sz.399001.csv # 深证成指 │ ├── sz.399300.csv # 沪深300 │ ├── sz.399006.csv # 创业板指 │ ├── sh.600000.csv # 浦发银行 │ └── ... (所有其他股票) ├── minute_5_k_data/ # 5分钟线数据结构同上 ├── logs/ # 存放运行日志便于排查问题 └── config.json # 配置文件存放股票列表、日期范围等在代码开头我们进行一些基础配置import baostock as bs import pandas as pd import os import time import concurrent.futures from datetime import datetime import logging # 配置日志 logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) logger logging.getLogger(__name__) # 本地存储根路径 BASE_DATA_PATH ./financial_data_local os.makedirs(BASE_DATA_PATH, exist_okTrue) os.makedirs(os.path.join(BASE_DATA_PATH, logs), exist_okTrue) # 定义需要下载的指数列表Baostock代码 INDEX_LIST [ (sh.000001, 上证指数), (sz.399001, 深证成指), (sz.399300, 沪深300), (sz.399006, 创业板指), # 可以在此添加其他指数如 sz.399005 (中小板指) ] # 定义K线类型和对应的存储目录及字段 DATA_CONFIG { daily: { directory: daily_k_data, fields: date,code,open,high,low,close,preclose,volume,amount,adjustflag,turn,tradestatus,pctChg,peTTM,pbMRQ,psTTM,pcfNcfTTM,isST }, 5min: { directory: minute_5_k_data, fields: date,time,code,open,high,low,close,volume,amount,adjustflag } }这里有几个关键点adjustflag字段这是Baostock数据的一个核心参数。adjustflag3代表后复权这是进行长期技术分析和回测最常用的数据因为它考虑了分红送股的影响使得价格曲线连续可比。除非你有特殊需求否则强烈建议使用后复权数据。字段选择query_history_k_data的fields参数决定了返回哪些列。日线数据我选择了包含价格、成交量、成交额、换手率、状态以及市盈率peTTM、市净率pbMRQ等基本面指标的全套字段以备不时之需。5分钟线则关注价格和成交量。3.2 核心下载函数处理单只股票/指数这是工具的基石函数负责与Baostock交互获取数据并保存。def download_k_data(code, name, start_date1990-12-19, end_datedatetime.now().strftime(%Y-%m-%d), frequencydaily, max_retry3): 下载单只股票或指数的K线数据。 Args: code: 证券代码如 sh.600000 name: 证券名称用于日志 start_date: 开始日期 end_date: 结束日期 frequency: K线类型daily 或 5min max_retry: 网络请求失败重试次数 data_config DATA_CONFIG.get(frequency) if not data_config: logger.error(f不支持的频率类型: {frequency}) return False save_dir os.path.join(BASE_DATA_PATH, data_config[directory]) os.makedirs(save_dir, exist_okTrue) save_path os.path.join(save_dir, f{code}.csv) # 如果文件已存在且本次下载的结束日期不晚于文件中最新的日期则跳过增量更新逻辑 if os.path.exists(save_path): try: existing_df pd.read_csv(save_path) if not existing_df.empty: last_date pd.to_datetime(existing_df[date]).max() if pd.to_datetime(end_date) last_date: logger.info(f{name}({code}) 本地数据已是最新跳过下载。) return True except Exception as e: logger.warning(f读取现有文件{save_path}失败将重新下载。错误: {e}) logger.info(f开始下载 {name}({code}) 的{frequency}数据时间范围: {start_date} 至 {end_date}) retry_count 0 while retry_count max_retry: try: # 登录 Baostock lg bs.login() if lg.error_code ! 0: logger.error(f登录失败: {lg.error_msg}) return False # 查询历史K线数据 # 对于分钟线start_date和end_date需要是具体日期且end_date不能是未来日期 rs bs.query_history_k_data_plus(code, data_config[fields], start_datestart_date, end_dateend_date, frequencyfrequency, # d 日线 5 5分钟 adjustflag3) # 复权状态3后复权 if rs.error_code ! 0: logger.error(f查询{name}({code})数据失败: {rs.error_msg}) bs.logout() return False # 将数据转换为pandas DataFrame data_list [] while (rs.error_code 0) rs.next(): data_list.append(rs.get_row_data()) df pd.DataFrame(data_list, columnsrs.fields) if df.empty: logger.warning(f{name}({code}) 在指定时间段内无数据。) bs.logout() # 保存一个空DataFrame或跳过保存这里选择保存空文件以记录状态。 df.to_csv(save_path, indexFalse) return True # 数据清洗确保数值列是数字类型 numeric_columns [open, high, low, close, preclose, volume, amount, turn, pctChg, peTTM, pbMRQ, psTTM, pcfNcfTTM] for col in numeric_columns: if col in df.columns: df[col] pd.to_numeric(df[col], errorscoerce) # 按日期排序 df[date] pd.to_datetime(df[date]) df df.sort_values(date).reset_index(dropTrue) # 保存到CSV df.to_csv(save_path, indexFalse) logger.info(f成功保存 {name}({code}) 数据至 {save_path}, 共 {len(df)} 条记录。) bs.logout() return True except Exception as e: retry_count 1 logger.error(f下载 {name}({code}) 第{retry_count}次失败: {e}) time.sleep(2 ** retry_count) # 指数退避策略避免频繁请求 logger.error(f下载 {name}({code}) 失败已达最大重试次数 {max_retry}。) return False这个函数包含了几个重要的工程化细节增量更新每次下载前检查本地文件的最新日期如果本地数据已经包含到目标结束日期则跳过下载节省时间和资源。错误重试与指数退避网络请求可能失败加入了重试机制。time.sleep(2 ** retry_count)是经典的指数退避失败后等待时间逐渐变长既给服务器喘息之机也提高重试成功率。数据清洗Baostock返回的数据是字符串格式必须转换为数值类型pd.to_numeric才能用于计算。errorscoerce会将无法转换的值设为NaN避免程序崩溃。排序确保数据按日期升序排列这是时间序列分析的前提。3.3 批量获取股票列表与并发下载下载完指数后我们需要获取全市场A股股票列表。Baostock提供了query_all_stock接口。def get_all_stock_codes(dateNone): 获取指定日期所有A股股票代码列表。 Args: date: 查询日期格式YYYY-MM-DD默认为当前日期。 Returns: list of tuples: [(code, name), ...] if date is None: date datetime.now().strftime(%Y-%m-%d) stock_list [] try: lg bs.login() rs bs.query_all_stock(date) if rs.error_code ! 0: logger.error(f获取股票列表失败: {rs.error_msg}) return [] while (rs.error_code 0) rs.next(): row rs.get_row_data() # 只保留沪深A股排除指数、基金、债券等。状态为‘1’表示上市 if row[0].startswith((sh.60, sh.68, sz.00, sz.30)) and row[2] 1: stock_list.append((row[0], row[1])) # (code, name) bs.logout() logger.info(f获取到 {len(stock_list)} 只A股股票。) except Exception as e: logger.error(f获取股票列表异常: {e}) return stock_list注意query_all_stock返回的是指定日期所有证券包括指数、基金、债券等。我们需要通过代码前缀sh.60上证主板sh.68科创板sz.00深证主板sz.30创业板和状态1代表上市来过滤出我们需要的A股股票。有了股票列表我们就可以进行批量并发下载了。直接使用concurrent.futures.ThreadPoolExecutor是最简单高效的方式。def batch_download_stocks(stock_list, start_date, end_date, frequencydaily, max_workers10): 并发批量下载股票数据。 Args: stock_list: 股票代码列表格式[(code, name), ...] start_date, end_date: 时间范围 frequency: K线类型 max_workers: 并发线程数 logger.info(f开始批量下载 {len(stock_list)} 只股票的{frequency}数据并发数{max_workers}。) success_count 0 fail_list [] with concurrent.futures.ThreadPoolExecutor(max_workersmax_workers) as executor: # 创建任务字典便于后续关联结果 future_to_stock { executor.submit(download_k_data, code, name, start_date, end_date, frequency): (code, name) for code, name in stock_list } for future in concurrent.futures.as_completed(future_to_stock): code, name future_to_stock[future] try: success future.result() if success: success_count 1 else: fail_list.append((code, name)) except Exception as e: logger.error(f处理 {name}({code}) 时发生未预料错误: {e}) fail_list.append((code, name)) logger.info(f批量下载完成。成功: {success_count}, 失败: {len(fail_list)}) if fail_list: logger.warning(失败的股票列表:) for code, name in fail_list: logger.warning(f {name}({code})) # 可以将失败列表保存到文件以便后续重试 retry_path os.path.join(BASE_DATA_PATH, logs, ffail_list_{datetime.now().strftime(%Y%m%d_%H%M%S)}.txt) with open(retry_path, w) as f: for code, name in fail_list: f.write(f{code},{name}\n) logger.info(f失败列表已保存至: {retry_path})这里将并发数max_workers设置为10这是一个比较保守的值既能显著提升速度又不会对Baostock服务器造成过大压力。请务必合理设置并发数做一个有节制的数据使用者。3.4 主程序流程与调度最后我们将所有模块组合起来形成一个完整的脚本。这个脚本可以按需运行比如每周五收盘后运行一次更新全市场数据。def main(): # 1. 登录主流程中统一登录一次但实际下载函数内会自己管理会话 # 注意由于我们采用并发且每个下载任务独立管理登录这里的主登录不是必须的但可以用于测试连通性。 try: lg bs.login() if lg.error_code ! 0: logger.critical(fBaostock 登录测试失败程序退出: {lg.error_msg}) return bs.logout() except Exception as e: logger.critical(f连接Baostock失败: {e}) return # 2. 下载主要指数 logger.info(开始下载主要指数数据...) for code, name in INDEX_LIST: download_k_data(code, name, start_date1990-12-19, frequencydaily) # 分钟线数据量巨大建议按需下载最近几个月或几年的 # download_k_data(code, name, start_date2023-01-01, frequency5min) # 3. 获取并下载全A股股票日线数据 logger.info(开始获取全A股股票列表...) all_stocks get_all_stock_codes() if not all_stocks: logger.error(未获取到股票列表程序终止。) return # 可以在这里对all_stocks进行切片例如 all_stocks[:50] 用于测试前50只股票 batch_download_stocks(all_stocks, start_date1990-12-19, end_date2024-05-17, frequencydaily) # 4. 可选下载5分钟线数据 - 数据量极大建议分批、分时间段进行 # logger.info(开始下载股票5分钟线数据最近一年...) # batch_download_stocks(all_stocks[:100], start_date2023-05-17, end_date2024-05-17, frequency5min, max_workers5) logger.info(所有数据下载任务调度完成。) if __name__ __main__: main()4. 实战中的坑与优化经验把代码跑起来只是第一步在实际运行和数据使用中我遇到了不少问题也总结出一些优化技巧。4.1 网络不稳定与请求超时处理Baostock的服务器偶尔会有响应慢或连接不稳定的情况。除了代码中已经实现的指数退避重试机制外还有两个关键点设置单次请求超时baostock库本身没有暴露连接超时参数。一个变通的方法是使用信号signal或者将下载函数放在具有超时控制的子线程中但这比较复杂。更务实的做法是控制单次请求的数据量。不要一次性请求某只股票从1990年至今的所有5分钟线这可能有数十万条记录这极易导致请求超时或内存溢出。应该按年或按月分批请求然后在本地合并。保存断点与重试清单batch_download_stocks函数已经实现了失败列表的记录。务必定期检查logs/fail_list_*.txt文件并编写一个简单的重试脚本专门读取这个文件再次尝试下载失败的项目。4.2 数据完整性校验下载完成后数据可能因为网络问题出现缺失。一个简单的校验方法是检查每只股票数据文件的时间序列连续性和记录数。def validate_data_integrity(data_dir, expected_start1990-12-19, expected_endNone): 简单校验数据完整性检查是否有文件以及日期范围是否大致合理。 import glob csv_files glob.glob(os.path.join(data_dir, *.csv)) logger.info(f在目录 {data_dir} 中找到 {len(csv_files)} 个数据文件。) problem_files [] for file in csv_files[:10]: # 抽样检查前10个文件 try: df pd.read_csv(file) if df.empty: problem_files.append((os.path.basename(file), 空文件)) continue df[date] pd.to_datetime(df[date]) actual_start df[date].min() actual_end df[date].max() # 这里可以根据实际情况设置合理的阈值进行判断 if actual_start pd.to_datetime(2005-01-01): # 例如如果数据最早日期晚于2005年可能有问题 problem_files.append((os.path.basename(file), f起始日期过晚: {actual_start})) except Exception as e: problem_files.append((os.path.basename(file), f读取错误: {e})) if problem_files: logger.warning(发现可能存在问题的文件:) for f, reason in problem_files: logger.warning(f {f}: {reason}) else: logger.info(抽样检查未发现明显问题。)更严格的校验可以对比股票数量或者用tushare等其它数据源进行抽样交叉验证。4.3 本地数据的高效读取与应用数据下载到本地后如何高效读取和使用是关键。直接使用pandas读取单个CSV很快但当你需要同时分析多只股票时频繁的I/O会成为瓶颈。场景一读取单只股票进行技术分析import pandas as pd import talib # 一个常用的技术指标库 def load_single_stock(code, frequencydaily): path f./financial_data_local/{frequency}_k_data/{code}.csv df pd.read_csv(path, parse_dates[date], index_coldate) df.sort_index(inplaceTrue) # 计算简单移动平均线 df[MA20] talib.SMA(df[close], timeperiod20) return df df_600000 load_single_stock(sh.600000) print(df_600000[[close, MA20]].tail())场景二批量读取多只股票进行因子计算使用HDF5或Parquet优化CSV适合存储但不适合高频次、多文件的随机读取。对于需要反复读取全市场数据的研究我强烈推荐将CSV数据转换并存储为HDF5或Parquet格式。这两种格式支持快速列式读取和压缩能极大提升性能。# 将CSV目录转换为一个HDF5文件只需执行一次 import pandas as pd import os store pd.HDFStore(./financial_data_local/all_stocks_daily.h5, modew) data_dir ./financial_data_local/daily_k_data for file in os.listdir(data_dir): if file.endswith(.csv): code file.replace(.csv, ) df pd.read_csv(os.path.join(data_dir, file), parse_dates[date], index_coldate) store.put(code, df, formattable, data_columnsTrue) # 以股票代码为键存储 store.close() # 使用时可以快速读取任意一只股票 store pd.HDFStore(./financial_data_local/all_stocks_daily.h5, moder) df_600000 store[sh.600000] store.close() # 或者读取多只股票到字典 codes_to_load [sh.600000, sz.000001] data_dict {code: store[code] for code in codes_to_load}使用HDF5后读取速度会有数量级的提升特别是当你的数据量达到GB级别时。4.4 内存管理与大规模数据处理下载全市场股票超过20年的日线数据CSV文件总大小可能达到几个GB。在进行处理时比如计算全市场所有股票的每日收益率需要留意内存使用。使用迭代器或分块处理pandas的read_csv函数支持chunksize参数可以分批读入大数据文件。使用dtype参数优化内存在读取CSV时明确指定每列的数据类型如{volume: float64, code: str}可以避免pandas进行类型推断节省内存。考虑使用数据库对于极其复杂或需要关联查询的场景可以将数据导入SQLite或PostgreSQL数据库。pandas的to_sql方法可以方便地实现这一点。数据库在复杂查询和索引方面有天然优势。5. 从数据到策略本地数据仓库的进阶用法拥有了本地的、结构化的数据仓库你的量化研究流程将变得非常流畅。这里分享两个进阶的应用思路。5.1 构建自定义指数有时你想跟踪某个特定板块或自己构建的投资组合的表现。利用本地数据你可以轻松计算自定义指数的日收益率。def calculate_custom_index(component_codes, weights, start_date, end_date): 计算自定义指数的每日收盘价序列。 component_codes: 成分股代码列表 weights: 权重列表需与component_codes一一对应且和为1 price_series [] for code in component_codes: df load_single_stock(code) # 使用前面定义的函数 # 确保数据在时间范围内并向前填充缺失值停牌处理 price_series.append(df.loc[start_date:end_date, close].fillna(methodffill)) # 将所有价格序列合并为一个DataFrame price_df pd.concat(price_series, axis1) price_df.columns component_codes # 计算指数加权平均 # 注意这里简化处理未考虑分红再投资和权重再平衡。实际中需要更复杂的逻辑。 custom_index (price_df * weights).sum(axis1) custom_index.name MyCustomIndex return custom_index5.2 简单的回测框架示例有了数据就可以进行策略回测。下面是一个极其简单的、用于演示概念的回测框架片段def simple_backtest(code, start_date, end_date, initial_capital100000): 一个简单的双均线策略回测。 df load_single_stock(code) df df.loc[start_date:end_date].copy() # 计算指标 df[MA_Short] df[close].rolling(window10).mean() df[MA_Long] df[close].rolling(window30).mean() # 生成交易信号 df[Signal] 0 df.loc[df[MA_Short] df[MA_Long], Signal] 1 # 金叉买入 df.loc[df[MA_Short] df[MA_Long], Signal] -1 # 死叉卖出 # 计算持仓和收益简化版未考虑手续费、滑点 df[Position] df[Signal].shift(1).fillna(0) # 第二天才能交易 df[Market_Return] df[close].pct_change() df[Strategy_Return] df[Position] * df[Market_Return] df[Strategy_Cumulative] (1 df[Strategy_Return]).cumprod() * initial_capital df[Market_Cumulative] (1 df[Market_Return]).cumprod() * initial_capital return df[[close, MA_Short, MA_Long, Signal, Position, Strategy_Cumulative, Market_Cumulative]]这个回测非常简陋真实的回测需要处理停牌、涨跌停、交易费用、资金管理等一系列复杂问题。但它的核心逻辑——基于本地数据快速计算信号和评估收益——已经体现出来。当你把数据放在本地迭代策略、调整参数的速度会非常快。整个工具从设计到实现再到优化和应用的思路就是这些。它不是一个复杂的系统但非常实用。它解决了一个具体而普遍的痛点让金融数据获取变得可管理、可复现、高效率。花一两天时间搭建好这个基础设施之后在策略研究的道路上你会感谢当初这个决定的。数据在手天下我有。剩下的就是发挥你的创造力在数据的海洋里寻找Alpha了。本文还有配套的精品资源点击获取